summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp17
-rw-r--r--apex/jobscheduler/framework/java/android/app/job/JobInfo.java6
-rw-r--r--core/api/current.txt2
-rw-r--r--core/api/system-current.txt1
-rw-r--r--core/api/test-current.txt1
-rw-r--r--core/java/android/animation/AnimationHandler.java6
-rw-r--r--core/java/android/app/ActivityManager.java13
-rw-r--r--core/java/android/app/ActivityManagerInternal.java8
-rw-r--r--core/java/android/app/AppCompatTaskInfo.java23
-rw-r--r--core/java/android/app/AppOpsManager.java11
-rw-r--r--core/java/android/app/IActivityManager.aidl4
-rw-r--r--core/java/android/app/TaskInfo.java6
-rw-r--r--core/java/android/content/pm/RegisteredServicesCache.java33
-rw-r--r--core/java/android/content/pm/UserInfo.java5
-rw-r--r--core/java/android/content/pm/multiuser.aconfig7
-rw-r--r--core/java/android/os/UserManager.java29
-rw-r--r--core/java/android/os/vibrator/flags.aconfig11
-rw-r--r--core/java/android/permission/flags.aconfig9
-rw-r--r--core/java/android/provider/Settings.java93
-rw-r--r--core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java4
-rw-r--r--core/java/android/view/LayoutInflater.java5
-rw-r--r--core/java/android/view/ListenerWrapper.java56
-rw-r--r--core/java/android/view/WindowManagerGlobal.java33
-rw-r--r--core/java/android/view/flags/view_flags.aconfig8
-rw-r--r--core/java/android/view/inspector/WindowInspector.java24
-rw-r--r--core/java/android/view/translation/ListenerGroup.java87
-rw-r--r--core/java/android/window/DesktopExperienceFlags.java16
-rw-r--r--core/java/android/window/WindowContainerTransaction.java32
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig7
-rw-r--r--core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java20
-rw-r--r--core/jni/com_android_internal_content_FileSystemUtils.cpp21
-rw-r--r--core/jni/com_android_internal_content_FileSystemUtils.h14
-rw-r--r--core/jni/com_android_internal_content_NativeLibraryHelper.cpp12
-rw-r--r--core/proto/android/providers/settings/secure.proto3
-rw-r--r--core/res/AndroidManifest.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_base.xml10
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_call.xml10
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_media.xml10
-rw-r--r--core/res/res/layout/notification_2025_text.xml8
-rw-r--r--core/res/res/layout/notification_template_notification_progress_bar.xml1
-rw-r--r--core/res/res/values/strings.xml2
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java6
-rw-r--r--core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java12
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java34
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt61
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt150
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt100
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt32
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt85
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt49
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt143
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt28
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt17
-rw-r--r--libs/hostgraphics/HostBufferQueue.cpp1
-rw-r--r--location/java/android/location/flags/location.aconfig7
-rw-r--r--media/java/android/media/AudioManager.java21
-rw-r--r--media/java/android/media/MediaCodecInfo.java145
-rw-r--r--media/java/android/media/tv/extension/scan/IScanListener.aidl2
-rw-r--r--media/java/android/media/tv/extension/scan/IScanSession.aidl2
-rw-r--r--media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl7
-rw-r--r--media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl4
-rw-r--r--native/android/performance_hint.cpp2
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java4
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java14
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java21
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java2
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java6
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java4
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt32
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt22
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt34
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt15
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt108
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json56
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json32
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json40
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json70
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json54
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt)2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java12
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt1046
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt1137
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt198
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt1077
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt385
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt142
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt64
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt96
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt102
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java6
-rw-r--r--packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml35
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml27
-rw-r--r--packages/SystemUI/res/layout/notif_half_shelf.xml15
-rw-r--r--packages/SystemUI/res/layout/notif_half_shelf_row.xml15
-rw-r--r--packages/SystemUI/res/layout/notification_2025_smart_action_button.xml35
-rw-r--r--packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml36
-rw-r--r--packages/SystemUI/res/values/dimens.xml4
-rw-r--r--packages/SystemUI/res/values/strings.xml4
-rw-r--r--packages/SystemUI/src/com/android/systemui/Dependency.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java (renamed from packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java)6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java79
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt126
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java88
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt77
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt (renamed from packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt)15
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt159
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt412
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt (renamed from packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt)25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt253
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt337
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt78
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt395
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt172
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt113
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt328
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt152
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt96
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt85
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt (renamed from packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt)4
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt24
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java21
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt114
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt41
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt21
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt22
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java22
-rwxr-xr-xravenwood/scripts/extract-last-soong-commands.py2
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java29
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java10
-rw-r--r--services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java1
-rw-r--r--services/core/java/com/android/server/adb/AdbDebuggingManager.java48
-rw-r--r--services/core/java/com/android/server/adb/AdbService.java38
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java18
-rw-r--r--services/core/java/com/android/server/am/BroadcastProcessQueue.java4
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueueImpl.java18
-rw-r--r--services/core/java/com/android/server/am/UserController.java56
-rw-r--r--services/core/java/com/android/server/am/UserSwitchingDialog.java27
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java8
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java11
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsRegistry.java100
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java7
-rw-r--r--services/core/java/com/android/server/display/DisplayControl.java13
-rw-r--r--services/core/java/com/android/server/display/VirtualDisplayAdapter.java34
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerServiceImpl.java4
-rw-r--r--services/core/java/com/android/server/pm/InstallRequest.java14
-rw-r--r--services/core/java/com/android/server/pm/UserTypeFactory.java31
-rw-r--r--services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java22
-rw-r--r--services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java109
-rw-r--r--services/core/java/com/android/server/selinux/flags.aconfig9
-rw-r--r--services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java61
-rw-r--r--services/core/java/com/android/server/tv/TvInputManagerService.java124
-rw-r--r--services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java141
-rw-r--r--services/core/java/com/android/server/updates/CertPinInstallReceiver.java20
-rw-r--r--services/core/java/com/android/server/vr/EnabledComponentsObserver.java62
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperCropper.java81
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDataParser.java8
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java207
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java102
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java16
-rw-r--r--services/core/java/com/android/server/wm/ActivityMetricsLogger.java11
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java24
-rw-r--r--services/core/java/com/android/server/wm/ActivitySnapshotController.java21
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java15
-rw-r--r--services/core/java/com/android/server/wm/AppCompatController.java8
-rw-r--r--services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java39
-rw-r--r--services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java5
-rw-r--r--services/core/java/com/android/server/wm/AppCompatUtils.java3
-rw-r--r--services/core/java/com/android/server/wm/BackNavigationController.java40
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java11
-rw-r--r--services/core/java/com/android/server/wm/DeferredDisplayUpdater.java26
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeHelper.java10
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java54
-rw-r--r--services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java31
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java26
-rw-r--r--services/core/java/com/android/server/wm/Task.java41
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java56
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java187
-rw-r--r--services/core/java/com/android/server/wm/Transition.java3
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java22
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java7
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java7
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java45
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java34
-rw-r--r--services/core/jni/com_android_server_display_DisplayControl.cpp7
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java9
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java3
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java45
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java101
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java340
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java108
-rw-r--r--services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java47
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java13
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java9
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java873
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java71
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java6
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java7
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java22
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskTests.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java15
-rw-r--r--telecomm/java/android/telecom/Connection.java4
-rw-r--r--tests/Input/src/com/android/server/input/BatteryControllerTests.kt4
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java8
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java10
330 files changed, 11291 insertions, 4312 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 4e05cbc23dc3..2dd16de3e188 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -73,6 +73,7 @@ aconfig_declarations_group {
"android.service.dreams.flags-aconfig-java",
"android.service.notification.flags-aconfig-java",
"android.service.quickaccesswallet.flags-aconfig-java",
+ "android.service.selinux.flags-aconfig-java",
"android.service.voice.flags-aconfig-java",
"android.speech.flags-aconfig-java",
"android.systemserver.flags-aconfig-java",
@@ -1943,3 +1944,19 @@ java_aconfig_library {
aconfig_declarations: "android.service.quickaccesswallet.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+
+// SELinux log collector
+aconfig_declarations {
+ name: "android.service.selinux.flags-aconfig",
+ package: "com.android.server.selinux.flags",
+ container: "system",
+ srcs: [
+ "services/core/java/com/android/server/selinux/*.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "android.service.selinux.flags-aconfig-java",
+ aconfig_declarations: "android.service.selinux.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index cc2d104813e4..d48af2cccd59 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -810,7 +810,7 @@ public class JobInfo implements Parcelable {
/**
* <p class="caution"><strong>Note:</strong> Beginning with
- * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+ * {@link android.os.Build.VERSION_CODES#BAKLAVA}, this flag will be ignored and no longer
* function effectively, regardless of the calling app's target SDK version.
* Calling this method will always return {@code false}.
*
@@ -2137,9 +2137,9 @@ public class JobInfo implements Parcelable {
* Jobs marked as important-while-foreground are given {@link #PRIORITY_HIGH} by default.
*
* <p class="caution"><strong>Note:</strong> Beginning with
- * {@link android.os.Build.VERSION_CODES#B}, this flag will be ignored and no longer
+ * {@link android.os.Build.VERSION_CODES#BAKLAVA}, this flag will be ignored and no longer
* function effectively, regardless of the calling app's target SDK version.
- * {link #isImportantWhileForeground()} will always return {@code false}.
+ * {@link #isImportantWhileForeground()} will always return {@code false}.
* Apps should use {link #setExpedited(boolean)} with {@code true} to indicate
* that this job is important and needs to run as soon as possible.
*
diff --git a/core/api/current.txt b/core/api/current.txt
index 7bc0fb220e1a..1630d80346ce 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -58287,7 +58287,9 @@ package android.view.inspector {
}
public final class WindowInspector {
+ method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void addGlobalWindowViewsListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.view.View>>);
method @NonNull public static java.util.List<android.view.View> getGlobalWindowViews();
+ method @FlaggedApi("android.view.flags.root_view_changed_listener") public static void removeGlobalWindowViewsListener(@NonNull java.util.function.Consumer<java.util.List<android.view.View>>);
}
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 984bc680c685..32b170a6286b 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11725,6 +11725,7 @@ package android.os {
field public static final String USER_TYPE_FULL_GUEST = "android.os.usertype.full.GUEST";
field public static final String USER_TYPE_FULL_SECONDARY = "android.os.usertype.full.SECONDARY";
field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM";
+ field @FlaggedApi("android.multiuser.allow_supervising_profile") public static final String USER_TYPE_PROFILE_SUPERVISING = "android.os.usertype.profile.SUPERVISING";
field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS";
}
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9c74471c65f1..bb023f25094e 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1199,6 +1199,7 @@ package android.content.pm {
method public boolean isProfile();
method public boolean isQuietModeEnabled();
method public boolean isRestricted();
+ method @FlaggedApi("android.multiuser.allow_supervising_profile") public boolean isSupervisingProfile();
method public boolean supportsSwitchTo();
method @Deprecated public boolean supportsSwitchToByUser();
method public void writeToParcel(android.os.Parcel, int);
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index d5b2f980e1a6..5f57a41675c9 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -110,7 +110,8 @@ public class AnimationHandler {
}
};
- public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
+ public static final ThreadLocal<AnimationHandler> sAnimatorHandler =
+ ThreadLocal.withInitial(AnimationHandler::new);
private static AnimationHandler sTestHandler = null;
private boolean mListDirty = false;
@@ -118,9 +119,6 @@ public class AnimationHandler {
if (sTestHandler != null) {
return sTestHandler;
}
- if (sAnimatorHandler.get() == null) {
- sAnimatorHandler.set(new AnimationHandler());
- }
return sAnimatorHandler.get();
}
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b38f5da6b638..54ab3b8f185b 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -55,9 +55,7 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
-import android.graphics.Rect;
import android.graphics.drawable.Icon;
-import android.hardware.HardwareBuffer;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Build;
@@ -86,7 +84,6 @@ import android.util.Log;
import android.util.Singleton;
import android.util.Size;
import android.view.WindowInsetsController.Appearance;
-import android.window.TaskSnapshot;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.LocalePicker;
@@ -5420,10 +5417,11 @@ public class ActivityManager {
*
* @hide
*/
+ @Nullable
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @Nullable String getSwitchingFromUserMessage() {
+ public String getSwitchingFromUserMessage(@UserIdInt int userId) {
try {
- return getService().getSwitchingFromUserMessage();
+ return getService().getSwitchingFromUserMessage(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -5434,10 +5432,11 @@ public class ActivityManager {
*
* @hide
*/
+ @Nullable
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @Nullable String getSwitchingToUserMessage() {
+ public String getSwitchingToUserMessage(@UserIdInt int userId) {
try {
- return getService().getSwitchingToUserMessage();
+ return getService().getSwitchingToUserMessage(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index a12c0674998e..e5f7889859c1 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -292,14 +292,14 @@ public abstract class ActivityManagerInternal {
public abstract boolean canStartMoreUsers();
/**
- * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}.
+ * Sets the user switcher message for switching from a user.
*/
- public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage);
+ public abstract void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message);
/**
- * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}.
+ * Sets the user switcher message for switching to a user.
*/
- public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage);
+ public abstract void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message);
/**
* Returns maximum number of users that can run simultaneously.
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 599f1a8be233..ea4646aa9eb9 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -102,6 +102,8 @@ public class AppCompatTaskInfo implements Parcelable {
private static final int FLAG_FULLSCREEN_OVERRIDE_USER = FLAG_BASE << 8;
/** Top activity flag for whether min aspect ratio of the activity has been overridden.*/
public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9;
+ /** Top activity flag for whether restart menu is shown due to display move. */
+ private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10;
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
@@ -115,7 +117,8 @@ public class AppCompatTaskInfo implements Parcelable {
FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON,
FLAG_FULLSCREEN_OVERRIDE_SYSTEM,
FLAG_FULLSCREEN_OVERRIDE_USER,
- FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE
+ FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE,
+ FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE
})
public @interface TopActivityFlag {}
@@ -133,7 +136,8 @@ public class AppCompatTaskInfo implements Parcelable {
@TopActivityFlag
private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED
- | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED;
+ | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED
+ | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE;
private AppCompatTaskInfo() {
// Do nothing
@@ -300,6 +304,21 @@ public class AppCompatTaskInfo implements Parcelable {
}
/**
+ * @return {@code true} if the restart menu is enabled for the top activity due to display move.
+ */
+ public boolean isRestartMenuEnabledForDisplayMove() {
+ return isTopActivityFlagEnabled(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE);
+ }
+
+ /**
+ * Sets the top activity flag for whether the restart menu is enabled for the top activity due
+ * to display move.
+ */
+ public void setRestartMenuEnabledForDisplayMove(boolean enable) {
+ setTopActivityFlag(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE, enable);
+ }
+
+ /**
* @return {@code true} if the top activity bounds are letterboxed.
*/
public boolean isTopActivityLetterboxed() {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 248f191cb8b8..1864d4a55f2e 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -18,7 +18,6 @@ package android.app;
import static android.location.flags.Flags.FLAG_LOCATION_BYPASS;
-import static android.media.audio.Flags.roForegroundAudioControl;
import static android.permission.flags.Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER;
import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS;
import static android.view.contentprotection.flags.Flags.FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED;
@@ -3481,6 +3480,16 @@ public class AppOpsManager {
}
/**
+ * Whether an app op is backed by a runtime permission or not.
+ * @hide
+ */
+ public static boolean opIsRuntimePermission(int op) {
+ if (op == OP_NONE) return false;
+
+ return ArrayUtils.contains(RUNTIME_PERMISSION_OPS, op);
+ }
+
+ /**
* Retrieve the user restriction associated with an operation, or null if there is not one.
* @hide
*/
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index ad01ad57b2d8..6cdfb97520ae 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -403,8 +403,8 @@ interface IActivityManager {
void setPackageScreenCompatMode(in String packageName, int mode);
@UnsupportedAppUsage
boolean switchUser(int userid);
- String getSwitchingFromUserMessage();
- String getSwitchingToUserMessage();
+ String getSwitchingFromUserMessage(int userId);
+ String getSwitchingToUserMessage(int userId);
@UnsupportedAppUsage
void setStopUserOnSwitch(int value);
boolean removeTask(int taskId);
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 6936ddc5eeac..59247934ba46 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -655,8 +655,10 @@ public class TaskInfo {
+ " effectiveUid=" + effectiveUid
+ " displayId=" + displayId
+ " isRunning=" + isRunning
- + " baseIntent=" + baseIntent + " baseActivity=" + baseActivity
- + " topActivity=" + topActivity + " origActivity=" + origActivity
+ + " baseIntent=" + baseIntent
+ + " baseActivity=" + baseActivity
+ + " topActivity=" + topActivity
+ + " origActivity=" + origActivity
+ " realActivity=" + realActivity
+ " numActivities=" + numActivities
+ " lastActiveTime=" + lastActiveTime
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 74da62c85ed2..10d3051cff6f 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -166,7 +166,15 @@ public abstract class RegisteredServicesCache<V> {
@UnsupportedAppUsage
public RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
- mContext = context;
+ this(new Injector<V>(context), interfaceName, metaDataName, attributeName,
+ serializerAndParser);
+ }
+
+ /** Provides the basic functionality for unit tests. */
+ @VisibleForTesting
+ public RegisteredServicesCache(Injector<V> injector, String interfaceName, String metaDataName,
+ String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
+ mContext = injector.getContext();
mInterfaceName = interfaceName;
mMetaDataName = metaDataName;
mAttributesName = attributeName;
@@ -184,7 +192,7 @@ public abstract class RegisteredServicesCache<V> {
if (isCore) {
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
}
- mBackgroundHandler = BackgroundThread.getHandler();
+ mBackgroundHandler = injector.getBackgroundHandler();
mContext.registerReceiverAsUser(
mPackageReceiver, UserHandle.ALL, intentFilter, null, mBackgroundHandler);
@@ -918,4 +926,25 @@ public abstract class RegisteredServicesCache<V> {
return null;
}
}
+
+ /**
+ * Point of injection for test dependencies.
+ * @param <V> The type of the value.
+ */
+ @VisibleForTesting
+ public static class Injector<V> {
+ private final Context mContext;
+
+ public Injector(Context context) {
+ mContext = context;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public Handler getBackgroundHandler() {
+ return BackgroundThread.getHandler();
+ }
+ }
}
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 582a1a9442ce..53203eba4020 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -410,6 +410,11 @@ public class UserInfo implements Parcelable {
return UserManager.isUserTypePrivateProfile(userType);
}
+ @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE)
+ public boolean isSupervisingProfile() {
+ return UserManager.isUserTypeSupervisingProfile(userType);
+ }
+
/** See {@link #FLAG_DISABLED}*/
@UnsupportedAppUsage
public boolean isEnabled() {
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 7f4fd3eff57e..7f57f5dbf0ab 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -629,3 +629,10 @@ flag {
description: "Enable moving content into the Private Space"
bug: "360066001"
}
+
+flag {
+ name: "allow_supervising_profile"
+ namespace: "supervision"
+ description: "Enables support for new supervising user type"
+ bug: "389712089"
+}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 767019d97758..c01c3cdc7ace 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -209,6 +209,23 @@ public class UserManager {
public static final String USER_TYPE_PROFILE_COMMUNAL = "android.os.usertype.profile.COMMUNAL";
/**
+ * User type representing a user who manages supervision on the device.
+ * When any full user on the device is supervised, the credentials for this profile will be
+ * required in order to perform certain actions for that user (i.e. those controlled by
+ * {@link android.app.supervision.SupervisionManager} or the
+ * {@link android.app.role.RoleManager#ROLE_SYSTEM_SUPERVISION supervision role holder}).
+ * There can only be one supervising profile per device, and the credentials set for that
+ * profile will be used to authorize actions for any supervised user on the device. This is
+ * distinct from a managed profile in that it functions only to authorize certain supervised
+ * actions; it does not represent the user to which restriction or management is applied.
+ * @hide
+ */
+ @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE)
+ @SystemApi
+ public static final String USER_TYPE_PROFILE_SUPERVISING =
+ "android.os.usertype.profile.SUPERVISING";
+
+ /**
* User type representing a {@link UserHandle#USER_SYSTEM system} user that is <b>not</b> a
* human user.
* This type of user cannot be created; it can only pre-exist on first boot.
@@ -3226,6 +3243,18 @@ public class UserManager {
}
/**
+ * Returns whether the user type is a
+ * {@link UserManager#USER_TYPE_PROFILE_SUPERVISING supervising profile}.
+ *
+ * @hide
+ */
+ @FlaggedApi(android.multiuser.Flags.FLAG_ALLOW_SUPERVISING_PROFILE)
+ @android.ravenwood.annotation.RavenwoodKeep
+ public static boolean isUserTypeSupervisingProfile(@Nullable String userType) {
+ return USER_TYPE_PROFILE_SUPERVISING.equals(userType);
+ }
+
+ /**
* @hide
* @deprecated Use {@link #isRestrictedProfile()}
*/
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 414f27498414..176c0c8ab966 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -165,3 +165,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "haptics"
+ name: "remove_hidl_support"
+ description: "Remove framework code to support HIDL vibrator HALs."
+ bug: "308452413"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 4bd54a173601..34272b17cf54 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -385,6 +385,15 @@ flag {
}
flag {
+ name: "record_all_runtime_appops_sqlite"
+ is_fixed_read_only: true
+ is_exported: true
+ namespace: "permissions"
+ description: "Enables recording of all runtime app ops into SQlite"
+ bug: "377584611"
+}
+
+flag {
name: "ranging_permission_enabled"
is_fixed_read_only: true
is_exported: true
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f91056dbce30..da4709b4b8b1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10617,6 +10617,57 @@ public final class Settings {
public static final String GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled";
/**
+ * Indicates that glanceable hub should never be started automatically.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_NEVER = 0;
+
+ /**
+ * Indicates that glanceable hub should be started when charging.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_CHARGING = 1;
+
+ /**
+ * Indicates that glanceable hub should be started when charging and upright.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_CHARGING_UPRIGHT = 2;
+
+ /**
+ * Indicates that glanceable hub should be started when docked.
+ *
+ * @hide
+ */
+ public static final int GLANCEABLE_HUB_START_DOCKED = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ GLANCEABLE_HUB_START_NEVER,
+ GLANCEABLE_HUB_START_CHARGING,
+ GLANCEABLE_HUB_START_CHARGING_UPRIGHT,
+ GLANCEABLE_HUB_START_DOCKED,
+ })
+ public @interface WhenToStartGlanceableHub {
+ }
+
+ /**
+ * Indicates when to start glanceable hub. Possible values are:
+ * 0: Never
+ * 1: While charging always
+ * 2: While upright and charging
+ * 3: While docked
+ *
+ * @hide
+ */
+ public static final String WHEN_TO_START_GLANCEABLE_HUB =
+ "when_to_start_glanceable_hub";
+
+ /**
* Whether home controls are enabled to be shown over the screensaver by the user.
*
* @hide
@@ -12533,6 +12584,48 @@ public final class Settings {
"accessibility_magnification_always_on_enabled";
/**
+ * Controls how the magnification follows the cursor.
+ *
+ * @hide
+ */
+ public static final String ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE =
+ "accessibility_magnification_cursor_following_mode";
+
+ /**
+ * Magnification cursor following mode value for the continuous mode.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS = 0;
+
+ /**
+ * Magnification cursor following mode value for the center mode.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER = 1;
+
+ /**
+ * Magnification cursor following mode value for the edge mode.
+ *
+ * @hide
+ */
+ public static final int ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE = 2;
+
+ /**
+ * Different cursor following settings that can be used as values with
+ * {@link #ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE}.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_" },
+ value = {
+ ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS,
+ ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER,
+ ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE})
+ public @interface AccessibilityMagnificationCursorFollowingMode {}
+
+ /**
* Whether the following typing focus feature for magnification is enabled.
* @hide
*/
diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
index e1dc6f6d642b..e9ddfc3559bf 100644
--- a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
+++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java
@@ -96,6 +96,10 @@ class QuickAccessWalletServiceInfo {
defaultAppPackageName = defaultPaymentApp.getPackageName();
}
+ if (defaultAppPackageName == null || defaultAppUser < 0) {
+ return null;
+ }
+
ServiceInfo serviceInfo = getWalletServiceInfo(context, defaultAppPackageName,
defaultAppUser);
if (serviceInfo == null) {
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
index aad3bf2679d9..901fc02f0040 100644
--- a/core/java/android/view/LayoutInflater.java
+++ b/core/java/android/view/LayoutInflater.java
@@ -516,8 +516,9 @@ public abstract class LayoutInflater {
mConstructorArgs[0] = inflaterContext;
View result = root;
- if (root != null && root.getViewRootImpl() != null) {
- root.getViewRootImpl().notifyRendererOfExpensiveFrame();
+ ViewRootImpl viewRootImpl = root != null ? root.getViewRootImpl() : null;
+ if (viewRootImpl != null) {
+ viewRootImpl.notifyRendererOfExpensiveFrame();
}
try {
diff --git a/core/java/android/view/ListenerWrapper.java b/core/java/android/view/ListenerWrapper.java
new file mode 100644
index 000000000000..fcf3fdb68112
--- /dev/null
+++ b/core/java/android/view/ListenerWrapper.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import android.annotation.NonNull;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * A utilty class to bundle a {@link Consumer} and an {@link Executor}
+ * @param <T> the type of value to be reported.
+ * @hide
+ */
+public class ListenerWrapper<T> {
+
+ @NonNull
+ private final Consumer<T> mConsumer;
+ @NonNull
+ private final Executor mExecutor;
+
+ public ListenerWrapper(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
+ mExecutor = Objects.requireNonNull(executor);
+ mConsumer = Objects.requireNonNull(consumer);
+ }
+
+ /**
+ * Relays the new value to the {@link Consumer} using the {@link Executor}
+ */
+ public void accept(@NonNull T value) {
+ mExecutor.execute(() -> mConsumer.accept(value));
+ }
+
+ /**
+ * Returns {@code true} if the consumer matches the one provided in the constructor,
+ * {@code false} otherwise.
+ */
+ public boolean isConsumerSame(@NonNull Consumer<T> consumer) {
+ return mConsumer.equals(consumer);
+ }
+}
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index a5da0c3ce5b1..624216776f42 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -44,6 +44,7 @@ import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.inputmethod.InputMethodManager;
+import android.view.translation.ListenerGroup;
import android.window.ITrustedPresentationListener;
import android.window.InputTransferToken;
import android.window.TrustedPresentationThresholds;
@@ -58,6 +59,7 @@ import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.List;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -147,6 +149,12 @@ public final class WindowManagerGlobal {
@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
+ /**
+ * The {@link ListenerGroup} that is associated to {@link #mViews}.
+ * @hide
+ */
+ @GuardedBy("mLock")
+ private final ListenerGroup<List<View>> mWindowViewsListenerGroup = new ListenerGroup<>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
@@ -319,6 +327,29 @@ public final class WindowManagerGlobal {
}
}
+ /**
+ * Adds a listener that will be notified whenever {@link #getWindowViews()} changes. The
+ * current value is provided immediately. If it was registered previously then this is ano op.
+ */
+ public void addWindowViewsListener(@NonNull Executor executor,
+ @NonNull Consumer<List<View>> consumer) {
+ synchronized (mLock) {
+ mWindowViewsListenerGroup.addListener(executor, consumer);
+ mWindowViewsListenerGroup.accept(getWindowViews());
+ }
+ }
+
+ /**
+ * Removes a listener that was registered in
+ * {@link #addWindowViewsListener(Executor, Consumer)}. If it was not registered previously,
+ * then this is a no op.
+ */
+ public void removeWindowViewsListener(@NonNull Consumer<List<View>> consumer) {
+ synchronized (mLock) {
+ mWindowViewsListenerGroup.removeListener(consumer);
+ }
+ }
+
public View getWindowView(IBinder windowToken) {
synchronized (mLock) {
final int numViews = mViews.size();
@@ -454,6 +485,7 @@ public final class WindowManagerGlobal {
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView, userId);
+ mWindowViewsListenerGroup.accept(getWindowViews());
} catch (RuntimeException e) {
Log.e(TAG, "Couldn't add view: " + view, e);
final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
@@ -575,6 +607,7 @@ public final class WindowManagerGlobal {
mDyingViews.remove(view);
}
allViewsRemoved = mRoots.isEmpty();
+ mWindowViewsListenerGroup.accept(getWindowViews());
}
// If we don't have any views anymore in our process, we no longer need the
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index d06f885638b6..d97310494d34 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -159,3 +159,11 @@ flag {
bug: "364653005"
is_fixed_read_only: true
}
+
+flag {
+ name: "root_view_changed_listener"
+ namespace: "windowing_sdk"
+ description: "Implement listener pattern for WindowInspector#getGlobalWindowViews."
+ bug: "394397033"
+ is_fixed_read_only: false
+}
diff --git a/core/java/android/view/inspector/WindowInspector.java b/core/java/android/view/inspector/WindowInspector.java
index 69d004e860fd..3ebca3c9d9b6 100644
--- a/core/java/android/view/inspector/WindowInspector.java
+++ b/core/java/android/view/inspector/WindowInspector.java
@@ -16,11 +16,14 @@
package android.view.inspector;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.view.View;
import android.view.WindowManagerGlobal;
import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Provides access to window inspection information.
@@ -37,4 +40,25 @@ public final class WindowInspector {
public static List<View> getGlobalWindowViews() {
return WindowManagerGlobal.getInstance().getWindowViews();
}
+
+ /**
+ * Adds a listener that is notified whenever the list of global window views changes. If a
+ * {@link Consumer} is already registered this method is a no op.
+ * @see #getGlobalWindowViews()
+ */
+ @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER)
+ public static void addGlobalWindowViewsListener(@NonNull Executor executor,
+ @NonNull Consumer<List<View>> consumer) {
+ WindowManagerGlobal.getInstance().addWindowViewsListener(executor, consumer);
+ }
+
+ /**
+ * Removes a listener from getting notifications of global window views changes. If the
+ * {@link Consumer} is not registered this method is a no op.
+ * @see #addGlobalWindowViewsListener(Executor, Consumer)
+ */
+ @FlaggedApi(android.view.flags.Flags.FLAG_ROOT_VIEW_CHANGED_LISTENER)
+ public static void removeGlobalWindowViewsListener(@NonNull Consumer<List<View>> consumer) {
+ WindowManagerGlobal.getInstance().removeWindowViewsListener(consumer);
+ }
}
diff --git a/core/java/android/view/translation/ListenerGroup.java b/core/java/android/view/translation/ListenerGroup.java
new file mode 100644
index 000000000000..bf506815f841
--- /dev/null
+++ b/core/java/android/view/translation/ListenerGroup.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.translation;
+
+import android.annotation.NonNull;
+import android.view.ListenerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * A utility class to manage a list of {@link ListenerWrapper}. This class is not thread safe.
+ * @param <T> the type of the value to be reported.
+ * @hide
+ */
+public class ListenerGroup<T> {
+ private final List<ListenerWrapper<T>> mListeners = new ArrayList<>();
+
+ /**
+ * Relays the value to all the registered {@link java.util.function.Consumer}
+ */
+ public void accept(@NonNull T value) {
+ Objects.requireNonNull(value);
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).accept(value);
+ }
+ }
+
+ /**
+ * Adds a {@link Consumer} to the group. If the {@link Consumer} is already present then this
+ * is a no op.
+ */
+ public void addListener(@NonNull Executor executor, @NonNull Consumer<T> consumer) {
+ if (isContained(consumer)) {
+ return;
+ }
+ mListeners.add(new ListenerWrapper<>(executor, consumer));
+ }
+
+ /**
+ * Removes a {@link Consumer} from the group. If the {@link Consumer} was not present then this
+ * is a no op.
+ */
+ public void removeListener(@NonNull Consumer<T> consumer) {
+ final int index = computeIndex(consumer);
+ if (index > -1) {
+ mListeners.remove(index);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@link Consumer} is present in the list, {@code false}
+ * otherwise.
+ */
+ private boolean isContained(Consumer<T> consumer) {
+ return computeIndex(consumer) > -1;
+ }
+
+ /**
+ * Returns the index of the matching {@link ListenerWrapper} if present, {@code -1} otherwise.
+ */
+ private int computeIndex(Consumer<T> consumer) {
+ for (int i = 0; i < mListeners.size(); i++) {
+ if (mListeners.get(i).isConsumerSame(consumer)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java
index cf582176a9f7..4f89c831a05f 100644
--- a/core/java/android/window/DesktopExperienceFlags.java
+++ b/core/java/android/window/DesktopExperienceFlags.java
@@ -47,17 +47,17 @@ public enum DesktopExperienceFlags {
com.android.server.display.feature.flags.Flags::baseDensityForExternalDisplays, true),
CONNECTED_DISPLAYS_CURSOR(com.android.input.flags.Flags::connectedDisplaysCursor, true),
DISPLAY_TOPOLOGY(com.android.server.display.feature.flags.Flags::displayTopology, true),
- ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, false),
+ ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, true),
ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false),
ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false),
- ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false),
+ ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, true),
ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(
com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement,
true),
- ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, false),
- ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, false),
+ ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, true),
+ ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, true),
ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true),
- ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false),
+ ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, true),
ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false),
ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false),
ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS(
@@ -66,9 +66,11 @@ public enum DesktopExperienceFlags {
false),
ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF(
Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false),
- ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false),
+ ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, false),
+ ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, true),
ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays,
- false),
+ true),
+ FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH(Flags::formFactorBasedDesktopFirstSwitch, false),
REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true)
// go/keep-sorted end
;
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 1156503cf8e8..ea345a5ef46a 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -688,12 +688,6 @@ public final class WindowContainerTransaction implements Parcelable {
@NonNull
public WindowContainerTransaction setAdjacentRoots(
@NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- mHierarchyOps.add(HierarchyOp.createForAdjacentRoots(
- root1.asBinder(),
- root2.asBinder()));
- return this;
- }
return setAdjacentRootSet(root1, root2);
}
@@ -714,10 +708,6 @@ public final class WindowContainerTransaction implements Parcelable {
*/
@NonNull
public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalArgumentException("allowMultipleAdjacentTaskFragments is not enabled."
- + " Use #setAdjacentRoots instead.");
- }
if (roots.length < 2) {
throw new IllegalArgumentException("setAdjacentRootSet must have size >= 2");
}
@@ -1973,13 +1963,6 @@ public final class WindowContainerTransaction implements Parcelable {
return mContainers;
}
- /** @deprecated b/373709676 replace with {@link #getContainers()}. */
- @Deprecated
- @NonNull
- public IBinder getAdjacentRoot() {
- return mReparent;
- }
-
public boolean getToTop() {
return mToTop;
}
@@ -2127,17 +2110,12 @@ public final class WindowContainerTransaction implements Parcelable {
sb.append(mContainer).append(" to ").append(mToTop ? "top" : "bottom");
break;
case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS:
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- for (IBinder container : mContainers) {
- if (container == mContainers[0]) {
- sb.append("adjacentRoots=").append(container);
- } else {
- sb.append(", ").append(container);
- }
+ for (IBinder container : mContainers) {
+ if (container == mContainers[0]) {
+ sb.append("adjacentRoots=").append(container);
+ } else {
+ sb.append(", ").append(container);
}
- } else {
- sb.append("container=").append(mContainer)
- .append(" adjacentRoot=").append(mReparent);
}
break;
case HIERARCHY_OP_TYPE_LAUNCH_TASK:
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index ce3a101a7519..e706af999117 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -938,3 +938,10 @@ flag {
description: "Enables the desktop-first mode switching logic based on its form factor."
bug: "394736817"
}
+
+flag {
+ name: "enable_restart_menu_for_connected_displays"
+ namespace: "lse_desktop_experience"
+ description: "Enable restart menu UI, which is shown when an app moves between displays."
+ bug: "397804287"
+}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 2d989943800e..6f7e5ad51b89 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -79,6 +79,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -864,5 +865,24 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen
throw new RuntimeException("Both mMessageString and mMessageHash should never be null");
}
}
+
+ /**
+ * This is only used by unit tests to wait until {@link #connectToConfigurationService} is
+ * done. Because unit tests are sensitive to concurrent accesses.
+ */
+ @VisibleForTesting
+ public static void waitForInitialization() {
+ final IProtoLog currentInstance = ProtoLog.getSingleInstance();
+ if (!(currentInstance instanceof PerfettoProtoLogImpl protoLog)) {
+ return;
+ }
+ try {
+ protoLog.mBackgroundLoggingService.submit(() -> {
+ Log.i(LOG_TAG, "Complete initialization");
+ }).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(LOG_TAG, "Failed to wait for tracing service", e);
+ }
+ }
}
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp
index 48c92c87f54e..886b9f1ae658 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.cpp
+++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp
@@ -201,8 +201,8 @@ bool punchHoles(const char *filePath, const uint64_t offset,
return true;
}
-bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
- std::vector<Elf64_Phdr> &programHeaders) {
+read_elf_status_t getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
+ std::vector<Elf64_Phdr> &programHeaders) {
// Open Elf file
Elf64_Ehdr ehdr;
std::ifstream inputStream(filePath, std::ifstream::in);
@@ -212,13 +212,13 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
// read executable headers
inputStream.read((char *)&ehdr, sizeof(ehdr));
if (!inputStream.good()) {
- return false;
+ return ELF_READ_ERROR;
}
- // only consider elf64 for punching holes
+ // only consider ELF64 files
if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
ALOGW("Provided file is not ELF64");
- return false;
+ return ELF_IS_NOT_64_BIT;
}
// read the program headers from elf file
@@ -229,7 +229,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
uint64_t phOffset;
if (__builtin_add_overflow(offset, programHeaderOffset, &phOffset)) {
ALOGE("Overflow occurred when calculating phOffset");
- return false;
+ return ELF_READ_ERROR;
}
inputStream.seekg(phOffset);
@@ -237,7 +237,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
Elf64_Phdr header;
inputStream.read((char *)&header, sizeof(header));
if (!inputStream.good()) {
- return false;
+ return ELF_READ_ERROR;
}
if (header.p_type != PT_LOAD) {
@@ -246,13 +246,14 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
programHeaders.push_back(header);
}
- return true;
+ return ELF_READ_OK;
}
bool punchHolesInElf64(const char *filePath, const uint64_t offset) {
std::vector<Elf64_Phdr> programHeaders;
- if (!getLoadSegmentPhdrs(filePath, offset, programHeaders)) {
- ALOGE("Failed to read program headers from ELF file.");
+ read_elf_status_t status = getLoadSegmentPhdrs(filePath, offset, programHeaders);
+ if (status != ELF_READ_OK) {
+ ALOGE("Failed to read program headers from 64 bit ELF file.");
return false;
}
return punchHoles(filePath, offset, programHeaders);
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.h b/core/jni/com_android_internal_content_FileSystemUtils.h
index 4a95686c5a0c..c4dc1115e6c4 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.h
+++ b/core/jni/com_android_internal_content_FileSystemUtils.h
@@ -22,6 +22,12 @@
namespace android {
+enum read_elf_status_t {
+ ELF_IS_NOT_64_BIT = -2,
+ ELF_READ_ERROR = -1,
+ ELF_READ_OK = 0,
+};
+
/*
* This function deallocates space used by zero padding at the end of LOAD segments in given
* uncompressed ELF file. Read ELF headers and find out the offset and sizes of LOAD segments.
@@ -39,10 +45,10 @@ bool punchHolesInElf64(const char* filePath, uint64_t offset);
bool punchHolesInZip(const char* filePath, uint64_t offset, uint16_t extraFieldLen);
/*
- * This function reads program headers from ELF file. ELF can be specified with file path directly
- * or it should be at offset inside Apk. Program headers passed to function is populated.
+ * This function reads program headers from 64 bit ELF file. ELF can be specified with file path
+ * directly or it should be at offset inside Apk. Program headers passed to function is populated.
*/
-bool getLoadSegmentPhdrs(const char* filePath, const uint64_t offset,
- std::vector<Elf64_Phdr>& programHeaders);
+read_elf_status_t getLoadSegmentPhdrs(const char* filePath, const uint64_t offset,
+ std::vector<Elf64_Phdr>& programHeaders);
} // namespace android \ No newline at end of file
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index 14132e61ff0e..7cf523f18a90 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -640,7 +640,17 @@ com_android_internal_content_NativeLibraryHelper_openApkFd(JNIEnv *env, jclass,
static jint checkLoadSegmentAlignment(const char* fileName, off64_t offset) {
std::vector<Elf64_Phdr> programHeaders;
- if (!getLoadSegmentPhdrs(fileName, offset, programHeaders)) {
+ read_elf_status_t status = getLoadSegmentPhdrs(fileName, offset, programHeaders);
+ // Ignore the ELFs which are not 64 bit.
+ if (status == ELF_IS_NOT_64_BIT) {
+ ALOGW("ELF file is not 64 Bit");
+ // PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED is equivalent of skipping the current file.
+ // on return, flag is OR'ed with flags from other ELF files. If some app has 32 bit ELF in
+ // 64 bit directory, alignment of that ELF will be ignored.
+ return PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED;
+ }
+
+ if (status == ELF_READ_ERROR) {
ALOGE("Failed to read program headers from ELF file.");
return PAGE_SIZE_APP_COMPAT_FLAG_ERROR;
}
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index ac4bac6d206e..1caa9e7af348 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -112,7 +112,8 @@ message SecureSettingsProto {
optional SettingProto autoclick_ignore_minor_cursor_movement = 63 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto autoclick_panel_position = 64 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto autoclick_revert_to_left_click = 65 [ (android.privacy).dest = DEST_AUTOMATIC ];
-
+ // Setting for accessibility magnification for cursor following mode.
+ optional SettingProto accessibility_magnification_cursor_following_mode = 66 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Accessibility accessibility = 2;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 36b65ba43162..e16ce9849ff2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -9292,9 +9292,6 @@
<action android:name="android.intent.action.UPDATE_PINS" />
<data android:scheme="content" android:host="*" android:mimeType="*/*" />
</intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
</receiver>
<receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver"
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index fe8875e413c4..63f32e3b3cd2 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -125,15 +125,7 @@
android:layout_marginBottom="@dimen/notification_2025_additional_margin"
android:minHeight="@dimen/notification_headerless_line_height"
>
- <!-- This is the simplest way to keep this text vertically centered without
- gravity="center_vertical" which causes jumpiness in expansion animations. -->
- <include
- layout="@layout/notification_2025_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="center_vertical"
- android:layout_marginTop="0dp"
- />
+ <include layout="@layout/notification_2025_text" />
</com.android.internal.widget.NotificationVanishingFrameLayout>
<include
diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml
index 9fb63f6eff13..fbea10d42b2b 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_call.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml
@@ -128,15 +128,7 @@
android:layout_height="wrap_content"
android:minHeight="@dimen/notification_headerless_line_height"
>
- <!-- This is the simplest way to keep this text vertically centered without
- gravity="center_vertical" which causes jumpiness in expansion animations. -->
- <include
- layout="@layout/notification_2025_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="center_vertical"
- android:layout_marginTop="0dp"
- />
+ <include layout="@layout/notification_2025_text" />
</com.android.internal.widget.NotificationVanishingFrameLayout>
</LinearLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 7e84292b0924..629af77b3dda 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -126,15 +126,7 @@
android:layout_marginBottom="@dimen/notification_2025_additional_margin"
android:layout_height="@dimen/notification_headerless_line_height"
>
- <!-- This is the simplest way to keep this text vertically centered without
- gravity="center_vertical" which causes jumpiness in expansion animations. -->
- <include
- layout="@layout/notification_template_text"
- android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="center_vertical"
- android:layout_marginTop="0dp"
- />
+ <include layout="@layout/notification_2025_text" />
</com.android.internal.widget.NotificationVanishingFrameLayout>
<include
diff --git a/core/res/res/layout/notification_2025_text.xml b/core/res/res/layout/notification_2025_text.xml
index 474f6d2099c6..a725de44b0bf 100644
--- a/core/res/res/layout/notification_2025_text.xml
+++ b/core/res/res/layout/notification_2025_text.xml
@@ -13,14 +13,16 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
+<!-- Note that we prefer layout_gravity="center_vertical" over gravity="center_vertical", since the
+ latter can cause jumpiness in expansion animations. -->
<com.android.internal.widget.ImageFloatingTextView
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.DeviceDefault.Notification.Text"
android:id="@+id/text"
android:layout_width="match_parent"
- android:layout_height="@dimen/notification_text_height"
- android:layout_gravity="top"
- android:layout_marginTop="@dimen/notification_text_margin_top"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_text_height"
+ android:layout_gravity="center_vertical"
android:ellipsize="end"
android:fadingEdge="horizontal"
android:gravity="top"
diff --git a/core/res/res/layout/notification_template_notification_progress_bar.xml b/core/res/res/layout/notification_template_notification_progress_bar.xml
index 35748962cfb2..8511d38718d1 100644
--- a/core/res/res/layout/notification_template_notification_progress_bar.xml
+++ b/core/res/res/layout/notification_template_notification_progress_bar.xml
@@ -20,4 +20,5 @@
android:layout_width="match_parent"
android:layout_height="@dimen/notification_progress_tracker_height"
style="@style/Widget.Material.Notification.NotificationProgressBar"
+ android:accessibilityLiveRegion="polite"
/>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index faa660a65d34..d94d659446ac 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6710,6 +6710,8 @@ ul.</string>
<string name="profile_label_test">Test</string>
<!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
<string name="profile_label_communal">Communal</string>
+ <!-- Supervising profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_supervising">Supervising</string>
<!-- Accessibility label for managed profile user type [CHAR LIMIT=30] -->
<string name="accessibility_label_managed_profile">Work profile</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ee74208e65c6..3de30f7f25a8 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1129,6 +1129,7 @@
<java-symbol type="string" name="profile_label_work_3" />
<java-symbol type="string" name="profile_label_test" />
<java-symbol type="string" name="profile_label_communal" />
+ <java-symbol type="string" name="profile_label_supervising" />
<java-symbol type="string" name="accessibility_label_managed_profile" />
<java-symbol type="string" name="accessibility_label_private_profile" />
<java-symbol type="string" name="accessibility_label_clone_profile" />
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index ee4761b9d024..dccbf4036b3e 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -245,6 +245,12 @@ public class RegisteredServicesCacheTest extends AndroidTestCase {
SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, new TestSerializer());
}
+ TestServicesCache(Injector<TestServiceType> injector,
+ XmlSerializerAndParser<TestServiceType> serializerAndParser) {
+ super(injector, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME,
+ serializerAndParser);
+ }
+
@Override
public TestServiceType parseServiceAttributes(Resources res, String packageName,
AttributeSet attrs) {
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 4d6c30ebbe2b..215c1623a530 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -541,24 +541,30 @@ public class WindowOnBackInvokedDispatcherTest {
throws RemoteException, InterruptedException {
// Setup a callback that unregisters itself after the gesture is finished but before the
// progress is animated back to 0f
- final AtomicBoolean unregisterOnProgressUpdate = new AtomicBoolean(false);
+ final AtomicBoolean unregisterOnNextCallbackInvocation = new AtomicBoolean(false);
final AtomicInteger onBackInvokedCalled = new AtomicInteger(0);
final CountDownLatch onBackCancelledCalled = new CountDownLatch(1);
OnBackAnimationCallback onBackAnimationCallback = new OnBackAnimationCallback() {
@Override
public void onBackProgressed(@NonNull BackEvent backEvent) {
- if (unregisterOnProgressUpdate.get()) {
+ if (unregisterOnNextCallbackInvocation.getAndSet(false)) {
mDispatcher.unregisterOnBackInvokedCallback(this);
}
}
@Override
public void onBackInvoked() {
+ if (unregisterOnNextCallbackInvocation.getAndSet(false)) {
+ mDispatcher.unregisterOnBackInvokedCallback(this);
+ }
onBackInvokedCalled.getAndIncrement();
}
@Override
public void onBackCancelled() {
+ if (unregisterOnNextCallbackInvocation.getAndSet(false)) {
+ mDispatcher.unregisterOnBackInvokedCallback(this);
+ }
onBackCancelledCalled.countDown();
}
};
@@ -572,7 +578,7 @@ public class WindowOnBackInvokedDispatcherTest {
// simulate back gesture finished and onBackCancelled() called, which starts the progress
// animation back to 0f. On the first progress emission, the callback will unregister itself
- unregisterOnProgressUpdate.set(true);
+ unregisterOnNextCallbackInvocation.set(true);
callbackInfo.getCallback().onBackCancelled();
waitForIdle();
onBackCancelledCalled.await(1000, TimeUnit.MILLISECONDS);
diff --git a/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 2e33253b5e09..ed5e0c608675 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
@@ -17,7 +17,9 @@
package com.android.wm.shell.shared.desktopmode;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
+import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE;
+import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement;
import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen;
import android.annotation.NonNull;
@@ -224,7 +226,7 @@ public class DesktopModeStatus {
/**
* Return {@code true} if the current device can host desktop sessions on its internal display.
*/
- public static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ private static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
}
@@ -269,6 +271,29 @@ public class DesktopModeStatus {
}
/**
+ * Check to see if a display should have desktop mode enabled or not. Internal
+ * and external displays have separate logic.
+ */
+ public static boolean isDesktopModeSupportedOnDisplay(Context context, Display display) {
+ if (!canEnterDesktopMode(context)) {
+ return false;
+ }
+ if (display.getType() == Display.TYPE_INTERNAL) {
+ return canInternalDisplayHostDesktops(context);
+ }
+
+ // TODO (b/395014779): Change this to use WM API
+ if ((display.getType() == Display.TYPE_EXTERNAL
+ || display.getType() == Display.TYPE_OVERLAY)
+ && enableDisplayContentModeManagement()) {
+ final WindowManager wm = context.getSystemService(WindowManager.class);
+ return wm != null && wm.shouldShowSystemDecors(display.getDisplayId());
+ }
+
+ return false;
+ }
+
+ /**
* Returns whether the multiple desktops feature is enabled for this device (both backend and
* frontend implementations).
*/
@@ -341,8 +366,11 @@ public class DesktopModeStatus {
if (!enforceDeviceRestrictions()) {
return true;
}
- final boolean desktopModeSupported = isDesktopModeSupported(context)
- && canInternalDisplayHostDesktops(context);
+ // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a
+ // requirement.
+ final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
+ ? isDesktopModeSupported(context) : (isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context));
final boolean desktopModeSupportedByDevOptions =
Flags.enableDesktopModeThroughDevOption()
&& isDesktopModeDevOptionSupported(context);
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt
index 0954b52ff151..ac54ac7a9d99 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/multiinstance/ManageWindowsViewContainer.kt
@@ -48,12 +48,14 @@ abstract class ManageWindowsViewContainer(
lateinit var menuView: ManageWindowsView
/** Creates the base menu view and fills it with icon views. */
- fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot>>,
+ fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot?>>,
onIconClickListener: ((Int) -> Unit),
onOutsideClickListener: (() -> Unit)): ManageWindowsView {
- val bitmapList = snapshotList.map { (index, snapshot) ->
- index to Bitmap.wrapHardwareBuffer(snapshot.hardwareBuffer, snapshot.colorSpace)
- }
+ val bitmapList = snapshotList
+ .filter { it.second != null }
+ .map { (index, snapshot) ->
+ index to Bitmap.wrapHardwareBuffer(snapshot!!.hardwareBuffer, snapshot.colorSpace)
+ }
return createAndShowMenuView(
bitmapList,
onIconClickListener,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index d9489287ff42..4f30a052de80 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -375,7 +375,7 @@ public class Bubble implements BubbleViewProvider {
user,
icon,
BubbleType.TYPE_APP,
- getAppBubbleKeyForApp(intent.getPackage(), user),
+ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
mainExecutor, bgExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 50e2f4d52bf2..3e95a0b1100f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1604,7 +1604,7 @@ public class BubbleController implements ConfigurationChangeListener,
@Nullable BubbleTransitions.DragData dragData) {
if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return;
Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow
- ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId);
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - taskId=%s", taskInfo.taskId);
BubbleBarLocation location = null;
if (dragData != null) {
location =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt
index 0d0bc9be72b3..9fefb515a539 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt
@@ -25,7 +25,8 @@ import com.android.wm.shell.ShellTaskOrganizer
object ComponentUtils {
/** Retrieves the package name from an [Intent]. */
@JvmStatic
- fun getPackageName(intent: Intent?): String? = intent?.component?.packageName
+ fun getPackageName(intent: Intent?): String? =
+ intent?.component?.packageName ?: intent?.`package`
/** Retrieves the package name from a [PendingIntent]. */
@JvmStatic
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 9b11e4ab16fa..4413c8715c0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.compatui;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.wm.shell.compatui.impl.CompatUIRequestsKt.DISPLAY_COMPAT_SHOW_RESTART_DIALOG;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
@@ -53,7 +55,9 @@ import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.api.CompatUIEvent;
import com.android.wm.shell.compatui.api.CompatUIHandler;
import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.api.CompatUIRequest;
import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.KeyguardChangeListener;
@@ -245,6 +249,21 @@ public class CompatUIController implements OnDisplaysChangedListener,
mCallback = callback;
}
+ @Override
+ public void sendCompatUIRequest(CompatUIRequest compatUIRequest) {
+ switch(compatUIRequest.getRequestId()) {
+ case DISPLAY_COMPAT_SHOW_RESTART_DIALOG:
+ handleDisplayCompatShowRestartDialog(compatUIRequest.asType());
+ break;
+ default:
+ }
+ }
+
+ private void handleDisplayCompatShowRestartDialog(
+ CompatUIRequests.DisplayCompatShowRestartDialog request) {
+ onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener()));
+ }
+
/**
* Called when the Task info changed. Creates and updates the compat UI if there is an
* activity in size compat, or removes the UI if there is no size compat activity.
@@ -254,13 +273,17 @@ public class CompatUIController implements OnDisplaysChangedListener,
public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) {
final TaskInfo taskInfo = compatUIInfo.getTaskInfo();
final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener();
- if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) {
+ final boolean isInDisplayCompatMode =
+ taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
+ if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()
+ && !isInDisplayCompatMode) {
mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
}
mIsInDesktopMode = isInDesktopMode(taskInfo);
// We close all the Compat UI educations in case TaskInfo has no configuration or
// TaskListener or in desktop mode.
- if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) {
+ if (taskInfo.configuration == null || taskListener == null
+ || (mIsInDesktopMode && !isInDisplayCompatMode)) {
// Null token means the current foreground activity is not in compatibility mode.
removeLayouts(taskInfo.taskId);
return;
@@ -552,8 +575,11 @@ public class CompatUIController implements OnDisplaysChangedListener,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
RestartDialogWindowManager layout =
mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
+ final boolean isInNonDisplayCompatDesktopMode = mIsInDesktopMode
+ && !taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
if (layout != null) {
- if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
+ if (layout.needsToBeRecreated(taskInfo, taskListener)
+ || isInNonDisplayCompatDesktopMode) {
mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
layout.release();
} else {
@@ -568,8 +594,9 @@ public class CompatUIController implements OnDisplaysChangedListener,
return;
}
}
- if (mIsInDesktopMode) {
- // Return if in desktop mode.
+ if (isInNonDisplayCompatDesktopMode) {
+ // No restart dialog can be shown in desktop mode unless the task is in display compat
+ // mode.
return;
}
// Create a new UI layout.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
index 817e554b550e..f71f8099f29f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
@@ -28,6 +28,11 @@ interface CompatUIHandler {
fun onCompatInfoChanged(compatUIInfo: CompatUIInfo)
/**
+ * Invoked when another component in Shell requests a CompatUI state change.
+ */
+ fun sendCompatUIRequest(compatUIRequest: CompatUIRequest)
+
+ /**
* Optional reference to the object responsible to send {@link CompatUIEvent}
*/
fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt
new file mode 100644
index 000000000000..069fd9b062a6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.api
+
+/**
+ * Abstraction for all the possible Compat UI Component requests.
+ */
+interface CompatUIRequest {
+ /**
+ * Unique request identifier
+ */
+ val requestId: Int
+
+ @Suppress("UNCHECKED_CAST")
+ fun <T : CompatUIRequest> asType(): T? = this as? T
+
+ fun <T : CompatUIRequest> asType(clazz: Class<T>): T? {
+ return if (clazz.isInstance(this)) clazz.cast(this) else null
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
new file mode 100644
index 000000000000..da4fc99491dc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.impl
+
+import android.app.TaskInfo
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.compatui.api.CompatUIRequest
+
+internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0
+
+/**
+ * All the {@link CompatUIRequest} the Compat UI Framework can handle
+ */
+sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest {
+ /** Sent when the restart handle menu is clicked, and a restart dialog is requested. */
+ data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo,
+ val taskListener: ShellTaskOrganizer.TaskListener) :
+ CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
index 02db85a4f99d..7dcb16c10097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
@@ -23,6 +23,7 @@ import com.android.wm.shell.compatui.api.CompatUIEvent
import com.android.wm.shell.compatui.api.CompatUIHandler
import com.android.wm.shell.compatui.api.CompatUIInfo
import com.android.wm.shell.compatui.api.CompatUIRepository
+import com.android.wm.shell.compatui.api.CompatUIRequest
import com.android.wm.shell.compatui.api.CompatUIState
import java.util.function.Consumer
@@ -102,4 +103,6 @@ class DefaultCompatUIHandler(
override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) {
this.compatUIEventSender = compatUIEventSender
}
+
+ override fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index d230425680ce..73be2d7ac49d 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
@@ -171,6 +171,7 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
+import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper;
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader;
import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier;
@@ -1021,6 +1022,7 @@ public abstract class WMShellModule {
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
AppToWebEducationController appToWebEducationController,
+ AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
FocusTransitionObserver focusTransitionObserver,
@@ -1044,10 +1046,10 @@ public abstract class WMShellModule {
rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser,
assistContentRequester, windowDecorViewHostSupplier, multiInstanceHelper,
desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
- windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
- focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
- taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy,
- desktopTilingDecorViewModel,
+ appHandleAndHeaderVisibilityHelper, windowDecorCaptionHandleRepository,
+ activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger,
+ desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler,
+ desktopModeCompatPolicy, desktopTilingDecorViewModel,
multiDisplayDragMoveIndicatorController));
}
@@ -1075,6 +1077,16 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
+ static AppHandleAndHeaderVisibilityHelper provideAppHandleAndHeaderVisibilityHelper(
+ @NonNull Context context,
+ @NonNull DisplayController displayController,
+ @NonNull DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ return new AppHandleAndHeaderVisibilityHelper(context, displayController,
+ desktopModeCompatPolicy);
+ }
+
+ @WMSingleton
+ @Provides
static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader(
@NonNull Context context, @NonNull ShellInit shellInit,
@NonNull ShellController shellController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 1f7edb413908..4646662073e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -452,6 +452,11 @@ class DesktopMixedTransitionHandler(
private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+ private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform
+ }
+
private fun findDesktopTaskLaunchChange(
info: TransitionInfo,
launchTaskId: Int?,
@@ -459,14 +464,18 @@ class DesktopMixedTransitionHandler(
return if (launchTaskId != null) {
// Launching a known task (probably from background or moving to front), so
// specifically look for it.
- findTaskChange(info, launchTaskId)
+ val launchChange = findTaskChange(info, launchTaskId)
+ if (
+ DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue &&
+ launchChange == null
+ ) {
+ findLaunchChange(info)
+ } else {
+ launchChange
+ }
} else {
// Launching a new task, so the first opening freeform task.
- info.changes.firstOrNull { change ->
- change.mode == TRANSIT_OPEN &&
- change.taskInfo != null &&
- change.taskInfo!!.isFreeform
- }
+ findLaunchChange(info)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt
index fbf170f13a40..91bd3c9b6c22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandler.kt
@@ -17,6 +17,7 @@
package com.android.wm.shell.desktopmode
import android.animation.Animator
+import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.os.IBinder
import android.view.Choreographer
@@ -28,9 +29,7 @@ import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.transition.Transitions
import kotlin.time.Duration.Companion.milliseconds
-/**
- * Transition handler for moving a window to a different display.
- */
+/** Transition handler for moving a window to a different display. */
class DesktopModeMoveToDisplayTransitionHandler(
private val animationTransaction: SurfaceControl.Transaction
) : Transitions.TransitionHandler {
@@ -47,46 +46,52 @@ class DesktopModeMoveToDisplayTransitionHandler(
finishTransaction: SurfaceControl.Transaction,
finishCallback: Transitions.TransitionFinishCallback,
): Boolean {
- val change = info.changes.find { it.startDisplayId != it.endDisplayId } ?: return false
- ValueAnimator.ofFloat(0f, 1f)
- .apply {
- duration = ANIM_DURATION.inWholeMilliseconds
- interpolator = Interpolators.LINEAR
- addUpdateListener { animation ->
- animationTransaction
- .setAlpha(change.leash, animation.animatedValue as Float)
- .setFrameTimeline(Choreographer.getInstance().vsyncId)
- .apply()
+ val changes = info.changes.filter { it.startDisplayId != it.endDisplayId }
+ if (changes.isEmpty()) return false
+ for (change in changes) {
+ val endBounds = change.endAbsBounds
+ // The position should be relative to the parent. For example, in ActivityEmbedding, the
+ // leash surface for the embedded Activity is parented to the container.
+ val endPosition = change.endRelOffset
+ startTransaction
+ .setPosition(change.leash, endPosition.x.toFloat(), endPosition.y.toFloat())
+ .setWindowCrop(change.leash, endBounds.width(), endBounds.height())
+ }
+ startTransaction.apply()
+
+ val animator = AnimatorSet()
+ animator.playTogether(
+ changes.map {
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = ANIM_DURATION.inWholeMilliseconds
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ animationTransaction
+ .setAlpha(it.leash, animation.animatedValue as Float)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
+ .apply()
+ }
}
- addListener(
- object : Animator.AnimatorListener {
- override fun onAnimationStart(animation: Animator) {
- val endBounds = change.endAbsBounds
- startTransaction
- .setPosition(
- change.leash,
- endBounds.left.toFloat(),
- endBounds.top.toFloat(),
- )
- .setWindowCrop(change.leash, endBounds.width(), endBounds.height())
- .apply()
- }
+ }
+ )
+ animator.addListener(
+ object : Animator.AnimatorListener {
+ override fun onAnimationStart(animation: Animator) = Unit
- override fun onAnimationEnd(animation: Animator) {
- finishTransaction.apply()
- finishCallback.onTransitionFinished(null)
- }
+ override fun onAnimationEnd(animation: Animator) {
+ finishTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ }
- override fun onAnimationCancel(animation: Animator) {
- finishTransaction.apply()
- finishCallback.onTransitionFinished(null)
- }
+ override fun onAnimationCancel(animation: Animator) {
+ finishTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ }
- override fun onAnimationRepeat(animation: Animator) = Unit
- }
- )
+ override fun onAnimationRepeat(animation: Animator) = Unit
}
- .start()
+ )
+ animator.start()
return true
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index c5ee3137e5ba..fa98d0339a65 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -22,6 +22,13 @@ import android.annotation.DimenRes
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
import android.content.pm.ActivityInfo.isFixedOrientationLandscape
import android.content.pm.ActivityInfo.isFixedOrientationPortrait
@@ -30,7 +37,9 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Rect
import android.os.SystemProperties
import android.util.Size
+import android.window.DesktopModeFlags
import com.android.wm.shell.R
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import kotlin.math.ceil
@@ -264,6 +273,58 @@ fun getAppHeaderHeight(context: Context): Int =
@DimenRes fun getAppHeaderHeightId(): Int = R.dimen.desktop_mode_freeform_decor_caption_height
/**
+ * Returns the task bounds a launching task should inherit from an existing running instance.
+ * Returns null if there are no bounds to inherit.
+ */
+fun getInheritedExistingTaskBounds(
+ taskRepository: DesktopRepository,
+ shellTaskOrganizer: ShellTaskOrganizer,
+ task: RunningTaskInfo,
+ deskId: Int,
+): Rect? {
+ if (!DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue) return null
+ val activeTask = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId).firstOrNull()
+ if (activeTask == null) return null
+ val lastTask = shellTaskOrganizer.getRunningTaskInfo(activeTask)
+ val lastTaskTopActivity = lastTask?.topActivity
+ val currentTaskTopActivity = task.topActivity
+ val intentFlags = task.baseIntent.flags
+ val launchMode = task.topActivityInfo?.launchMode ?: LAUNCH_MULTIPLE
+ return when {
+ // No running task activity to inherit bounds from.
+ lastTaskTopActivity == null -> null
+ // No current top activity to set bounds for.
+ currentTaskTopActivity == null -> null
+ // Top task is not an instance of the launching activity, do not inherit its bounds.
+ lastTaskTopActivity.packageName != currentTaskTopActivity.packageName -> null
+ // Top task is an instance of launching activity. Activity will be launching in a new
+ // task with the existing task also being closed. Inherit existing task bounds to
+ // prevent new task jumping.
+ (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) ->
+ lastTask.configuration.windowConfiguration.bounds
+ else -> null
+ }
+}
+
+/**
+ * Returns true if the launch mode or intent will result in a new task being created for the
+ * activity.
+ */
+private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) =
+ launchMode == LAUNCH_SINGLE_TASK ||
+ launchMode == LAUNCH_SINGLE_INSTANCE ||
+ launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK ||
+ (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0
+
+/**
+ * Returns true if the intent will result in an existing task instance being closed if a new one
+ * appears.
+ */
+private fun isClosingExitingInstance(intentFlags: Int) =
+ (intentFlags and FLAG_ACTIVITY_CLEAR_TASK) != 0 ||
+ (intentFlags and FLAG_ACTIVITY_MULTIPLE_TASK) == 0
+
+/**
* Calculates the desired initial bounds for applications in desktop windowing. This is done as a
* scale of the screen bounds.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 2835a18cd809..dbc599be57af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -32,6 +32,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
@@ -238,6 +240,10 @@ class DesktopTasksController(
removeVisualIndicator()
}
+ override fun onTransitionInterrupted() {
+ removeVisualIndicator()
+ }
+
private fun removeVisualIndicator() {
visualIndicator?.fadeOutIndicator { releaseVisualIndicator() }
}
@@ -1198,6 +1204,11 @@ class DesktopTasksController(
return
}
+ if (splitScreenController.isTaskInSplitScreen(task.taskId)) {
+ moveSplitPairToDisplay(task, displayId)
+ return
+ }
+
val wct = WindowContainerTransaction()
val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
if (displayAreaInfo == null) {
@@ -1205,39 +1216,6 @@ class DesktopTasksController(
return
}
- // check if the task is part of splitscreen
- if (
- Flags.enableNonDefaultDisplaySplit() &&
- Flags.enableMoveToNextDisplayShortcut() &&
- splitScreenController.isTaskInSplitScreen(task.taskId)
- ) {
- val activeDeskId = taskRepository.getActiveDeskId(displayId)
- logV("moveToDisplay: moving split root to displayId=%d", displayId)
- val stageCoordinatorRootTaskToken =
- splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(
- DEFAULT_DISPLAY
- )
- wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
- val deactivationRunnable =
- if (activeDeskId != null) {
- // Split is being placed on top of an existing desk in the target display. Make
- // sure it is cleaned up.
- performDesktopExitCleanUp(
- wct = wct,
- deskId = activeDeskId,
- displayId = displayId,
- willExitDesktop = true,
- shouldEndUpAtHome = false,
- )
- } else {
- null
- }
- val transition =
- transitions.startTransition(TRANSIT_CHANGE, wct, moveToDisplayTransitionHandler)
- deactivationRunnable?.invoke(transition)
- return
- }
-
val destinationDeskId = taskRepository.getDefaultDeskId(displayId)
if (destinationDeskId == null) {
logW("moveToDisplay: desk not found for display: $displayId")
@@ -1299,6 +1277,53 @@ class DesktopTasksController(
}
/**
+ * Move split pair associated with the [task] to display with [displayId].
+ *
+ * No-op if task is already on that display per [RunningTaskInfo.displayId].
+ */
+ private fun moveSplitPairToDisplay(task: RunningTaskInfo, displayId: Int) {
+ if (!splitScreenController.isTaskInSplitScreen(task.taskId)) {
+ return
+ }
+
+ if (!Flags.enableNonDefaultDisplaySplit() || !Flags.enableMoveToNextDisplayShortcut()) {
+ return
+ }
+
+ val wct = WindowContainerTransaction()
+ val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
+ if (displayAreaInfo == null) {
+ logW("moveSplitPairToDisplay: display not found")
+ return
+ }
+
+ val activeDeskId = taskRepository.getActiveDeskId(displayId)
+ logV("moveSplitPairToDisplay: moving split root to displayId=%d", displayId)
+
+ val stageCoordinatorRootTaskToken =
+ splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(DEFAULT_DISPLAY)
+ wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
+
+ val deactivationRunnable =
+ if (activeDeskId != null) {
+ // Split is being placed on top of an existing desk in the target display. Make
+ // sure it is cleaned up.
+ performDesktopExitCleanUp(
+ wct = wct,
+ deskId = activeDeskId,
+ displayId = displayId,
+ willExitDesktop = true,
+ shouldEndUpAtHome = false,
+ )
+ } else {
+ null
+ }
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
+ deactivationRunnable?.invoke(transition)
+ return
+ }
+
+ /**
* Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
* bounds) and a free floating state (either the last saved bounds if available or the default
* bounds otherwise).
@@ -2261,11 +2286,19 @@ class DesktopTasksController(
wct.reorder(task.token, true)
return wct
}
+ val inheritedTaskBounds =
+ getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
+ if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) {
+ // Inherit bounds from closing task instance to prevent application jumping different
+ // cascading positions.
+ wct.setBounds(task.token, inheritedTaskBounds)
+ }
// TODO(b/365723620): Handle non running tasks that were launched after reboot.
// If task is already visible, it must have been handled already and added to desktop mode.
- // Cascade task only if it's not visible yet.
+ // Cascade task only if it's not visible yet and has no inherited bounds.
if (
- DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
+ inheritedTaskBounds == null &&
+ DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
!taskRepository.isVisibleTask(task.taskId)
) {
val displayLayout = displayController.getDisplayLayout(task.displayId)
@@ -2501,9 +2534,17 @@ class DesktopTasksController(
) {
val targetDisplayId = taskRepository.getDisplayForDesk(deskId)
val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return
- val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId)
- if (canChangeTaskPosition(task)) {
- wct.setBounds(task.token, initialBounds)
+ val inheritedTaskBounds =
+ getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
+ if (inheritedTaskBounds != null) {
+ // Inherit bounds from closing task instance to prevent application jumping different
+ // cascading positions.
+ wct.setBounds(task.token, inheritedTaskBounds)
+ } else {
+ val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId)
+ if (canChangeTaskPosition(task)) {
+ wct.setBounds(task.token, initialBounds)
+ }
}
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 4f511a901756..24b2e4879546 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -24,8 +24,10 @@ import android.os.SystemClock
import android.os.SystemProperties
import android.os.UserHandle
import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CLOSE
import android.window.DesktopModeFlags
+import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.TransitionRequestInfo
@@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler(
*/
fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
if (!inProgress) {
+ logV("finishDragToDesktop: not in progress, returning")
// Don't attempt to finish a drag to desktop transition since there is no transition in
// progress which means that the drag to desktop transition was never successfully
// started.
return null
}
- if (requireTransitionState().startAborted) {
+ val state = requireTransitionState()
+ if (state.startAborted) {
+ logV("finishDragToDesktop: start was aborted, clearing state")
// Don't attempt to complete the drag-to-desktop since the start transition didn't
// succeed as expected. Just reset the state as if nothing happened.
clearState()
return null
}
- return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ if (state.startInterrupted) {
+ logV("finishDragToDesktop: start was interrupted, returning")
+ // We should only have interrupted the start transition after receiving a cancel/end
+ // request, let that existing request play out and just return here.
+ return null
+ }
+ state.endTransitionToken =
+ transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ return state.endTransitionToken
}
/**
@@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler(
clearState()
return
}
+ if (state.startInterrupted) {
+ // We should only have interrupted the start transition after receiving a cancel/end
+ // request, let that existing request play out and just return here.
+ return
+ }
state.cancelState = cancelState
if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
@@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler(
// transient to start and merge. Animate the cancellation (scale back to original
// bounds) first before actually starting the cancel transition so that the wallpaper
// is visible behind the animating task.
- startCancelAnimation()
+ state.activeCancelAnimation = startCancelAnimation()
} else if (
state.draggedTaskChange != null &&
(cancelState == CancelState.CANCEL_SPLIT_LEFT ||
@@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler(
) {
if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
// TODO(b/388853233): add support for dragging split task to bubble
- startCancelAnimation()
+ state.activeCancelAnimation = startCancelAnimation()
} else {
// Animation is handled by BubbleController
val wct = WindowContainerTransaction()
@@ -357,6 +375,19 @@ sealed class DragToDesktopTransitionHandler(
): Boolean {
val state = requireTransitionState()
+ if (
+ handleCancelOrExitAfterInterrupt(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ state,
+ )
+ ) {
+ return true
+ }
+
val isStartDragToDesktop =
info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
transition == state.startTransitionToken
@@ -539,6 +570,58 @@ sealed class DragToDesktopTransitionHandler(
}
}
+ private fun handleCancelOrExitAfterInterrupt(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ state: TransitionState,
+ ): Boolean {
+ if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
+ return false
+ }
+ val isCancelDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
+ transition == state.cancelTransitionToken
+ val isEndDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
+ transition == state.endTransitionToken
+ // We should only receive cancel or end transitions through startAnimation() if the
+ // start transition was interrupted while a cancel- or end-transition had already
+ // been requested. Finish the cancel/end transition to avoid having to deal with more
+ // incoming transitions, and clear the state for the next start-drag transition.
+ if (!isCancelDragToDesktop && !isEndDragToDesktop) {
+ return false
+ }
+ if (!state.startInterrupted) {
+ logW(
+ "Not interrupted, but received startAnimation for cancel/end drag." +
+ "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
+ )
+ return false
+ }
+ logV(
+ "startAnimation: interrupted -> " +
+ "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
+ )
+ if (isEndDragToDesktop) {
+ setupEndDragToDesktop(info, startTransaction, finishTransaction)
+ animateEndDragToDesktop(startTransaction = startTransaction, finishCallback)
+ } else { // isCancelDragToDesktop
+ // Similar to when we merge the cancel transition: ensure all tasks involved in the
+ // cancel transition are shown, and finish the transition immediately.
+ info.changes.forEach { change ->
+ startTransaction.show(change.leash)
+ finishTransaction.show(change.leash)
+ }
+ }
+ startTransaction.apply()
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ clearState()
+ return true
+ }
+
/**
* Calculates start drag to desktop layers for transition [info]. The leash layer is calculated
* based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is
@@ -590,6 +673,7 @@ sealed class DragToDesktopTransitionHandler(
?: error("Start transition expected to be waiting for merge but wasn't")
if (isEndTransition) {
logV("mergeAnimation: end-transition, target=$mergeTarget")
+ state.mergedEndTransition = true
setupEndDragToDesktop(
info,
startTransaction = startT,
@@ -617,6 +701,41 @@ sealed class DragToDesktopTransitionHandler(
return
}
logW("unhandled merge transition: transitionInfo=$info")
+ // Handle unknown incoming transitions by finishing the start transition. For now, only do
+ // this if we've already requested a cancel- or end transition. If we've already merged the
+ // end-transition, or if the end-transition is running on its own, then just wait until that
+ // finishes instead. If we've merged the cancel-transition we've finished the
+ // start-transition and won't reach this code.
+ if (
+ mergeTarget == state.startTransitionToken &&
+ isCancelOrEndTransitionRequested(state) &&
+ !state.mergedEndTransition
+ ) {
+ interruptStartTransition(state)
+ }
+ }
+
+ private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean =
+ state.cancelTransitionToken != null || state.endTransitionToken != null
+
+ private fun interruptStartTransition(state: TransitionState) {
+ if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
+ return
+ }
+ logV("interruptStartTransition")
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
+ state.dragAnimator.cancelAnimator()
+ state.activeCancelAnimation?.removeAllListeners()
+ state.activeCancelAnimation?.cancel()
+ state.activeCancelAnimation = null
+ // Keep the transition state so we can deal with Cancel/End properly in #startAnimation.
+ state.startInterrupted = true
+ dragToDesktopStateListener?.onTransitionInterrupted()
+ // Cancel CUJs here as they won't be accurate now that an incoming transition is playing.
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
+ LatencyTracker.getInstance(context)
+ .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
}
protected open fun setupEndDragToDesktop(
@@ -783,7 +902,7 @@ sealed class DragToDesktopTransitionHandler(
} ?: false
}
- private fun startCancelAnimation() {
+ private fun startCancelAnimation(): Animator {
val state = requireTransitionState()
val dragToDesktopAnimator = state.dragAnimator
@@ -800,7 +919,7 @@ sealed class DragToDesktopTransitionHandler(
val dx = targetX - x
val dy = targetY - y
val tx: SurfaceControl.Transaction = transactionSupplier.get()
- ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
+ return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
.setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
.apply {
addUpdateListener { animator ->
@@ -818,6 +937,7 @@ sealed class DragToDesktopTransitionHandler(
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
+ state.activeCancelAnimation = null
dragToDesktopStateListener?.onCancelToDesktopAnimationEnd()
// Start the cancel transition to restore order.
startCancelDragToDesktopTransition()
@@ -910,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler(
val dragLayer: Int,
)
+ /** Listener for various events happening during the DragToDesktop transition. */
interface DragToDesktopStateListener {
+ /** Indicates that the animation into Desktop has started. */
fun onCommitToDesktopAnimationStart()
+ /** Called when the animation to cancel the desktop-drag has finished. */
fun onCancelToDesktopAnimationEnd()
+
+ /** Indicates that the drag-to-desktop transition has been interrupted. */
+ fun onTransitionInterrupted()
}
sealed class TransitionState {
@@ -930,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler(
abstract var cancelState: CancelState
abstract var startAborted: Boolean
abstract val visualIndicator: DesktopModeVisualIndicator?
+ abstract var startInterrupted: Boolean
+ abstract var endTransitionToken: IBinder?
+ abstract var mergedEndTransition: Boolean
+ abstract var activeCancelAnimation: Animator?
data class FromFullscreen(
override val draggedTaskId: Int,
@@ -945,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler(
override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
override val visualIndicator: DesktopModeVisualIndicator?,
+ override var startInterrupted: Boolean = false,
+ override var endTransitionToken: IBinder? = null,
+ override var mergedEndTransition: Boolean = false,
+ override var activeCancelAnimation: Animator? = null,
var otherRootChanges: MutableList<Change> = mutableListOf(),
) : TransitionState()
@@ -962,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler(
override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
override val visualIndicator: DesktopModeVisualIndicator?,
+ override var startInterrupted: Boolean = false,
+ override var endTransitionToken: IBinder? = null,
+ override var mergedEndTransition: Boolean = false,
+ override var activeCancelAnimation: Animator? = null,
var splitRootChange: Change? = null,
var otherSplitTask: Int,
) : TransitionState()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
index 01fc6440712d..adc5cdf340fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
@@ -43,7 +43,7 @@ class DesktopHandleManageWindowsMenu(
private val captionWidth: Int,
private val windowManagerWrapper: WindowManagerWrapper,
context: Context,
- snapshotList: List<Pair<Int, TaskSnapshot>>,
+ snapshotList: List<Pair<Int, TaskSnapshot?>>,
onIconClickListener: ((Int) -> Unit),
onOutsideClickListener: (() -> Unit)
) : ManageWindowsViewContainer(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
index 02a5433147ca..3a75933f59d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
@@ -54,7 +54,7 @@ class DesktopHeaderManageWindowsMenu(
private val desktopUserRepositories: DesktopUserRepositories,
private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>,
private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
- snapshotList: List<Pair<Int, TaskSnapshot>>,
+ snapshotList: List<Pair<Int, TaskSnapshot?>>,
onIconClickListener: ((Int) -> Unit),
onOutsideClickListener: (() -> Unit)
) : ManageWindowsViewContainer(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 05185cc5cdf0..a1d2774ee428 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -17,11 +17,9 @@
package com.android.wm.shell.windowdecor;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
@@ -79,7 +77,6 @@ import android.view.SurfaceControl.Transaction;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewRootImpl;
-import android.view.WindowManager;
import android.window.DesktopModeFlags;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
@@ -121,7 +118,6 @@ import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction;
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt;
@@ -146,6 +142,7 @@ import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener;
+import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper;
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
@@ -207,6 +204,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter;
private final AppHandleEducationController mAppHandleEducationController;
private final AppToWebEducationController mAppToWebEducationController;
+ private final AppHandleAndHeaderVisibilityHelper mAppHandleAndHeaderVisibilityHelper;
private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private boolean mTransitionDragActive;
@@ -294,6 +292,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
AppToWebEducationController appToWebEducationController,
+ AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
FocusTransitionObserver focusTransitionObserver,
@@ -338,6 +337,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
desktopTasksLimiter,
appHandleEducationController,
appToWebEducationController,
+ appHandleAndHeaderVisibilityHelper,
windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
new TaskPositionerFactory(),
@@ -386,6 +386,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
AppToWebEducationController appToWebEducationController,
+ AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
TaskPositionerFactory taskPositionerFactory,
@@ -431,6 +432,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDesktopTasksLimiter = desktopTasksLimiter;
mAppHandleEducationController = appHandleEducationController;
mAppToWebEducationController = appToWebEducationController;
+ mAppHandleAndHeaderVisibilityHelper = appHandleAndHeaderVisibilityHelper;
mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mAssistContentRequester = assistContentRequester;
@@ -528,6 +530,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
@Override
public void setSplitScreenController(SplitScreenController splitScreenController) {
mSplitScreenController = splitScreenController;
+ mAppHandleAndHeaderVisibilityHelper.setSplitScreenController(splitScreenController);
}
@Override
@@ -1724,32 +1727,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
}
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
- if (mDisplayController.getDisplay(taskInfo.displayId) == null) {
- // If DisplayController doesn't have it tracked, it could be a private/managed display.
- return false;
- }
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
- if (mSplitScreenController != null
- && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) {
- return false;
- }
- if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) {
- return false;
- }
- final boolean isOnLargeScreen =
- mDisplayController.getDisplay(taskInfo.displayId).getMinSizeDimensionDp()
- >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
- if (!DesktopModeStatus.canEnterDesktopMode(mContext)
- && DesktopModeStatus.overridesShowAppHandle(mContext) && !isOnLargeScreen) {
- // Devices with multiple screens may enable the app handle but it should not show on
- // small screens
- return false;
- }
- return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext)
- && !DesktopWallpaperActivity.isWallpaperTask(taskInfo)
- && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
- && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
- && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop();
+ return mAppHandleAndHeaderVisibilityHelper.shouldShowAppHandleOrHeader(taskInfo);
}
private void createWindowDecoration(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index ce786177290f..c544468f5191 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -39,7 +39,6 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Space
-import android.widget.TextView
import android.window.DesktopModeFlags
import android.window.SurfaceSyncGroup
import androidx.annotation.StringRes
@@ -59,10 +58,10 @@ import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.DrawableInsets
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.calculateMenuPosition
-import com.android.wm.shell.windowdecor.common.DrawableInsets
-import com.android.wm.shell.windowdecor.common.createRippleDrawable
+import com.android.wm.shell.windowdecor.common.createBackgroundDrawable
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.isPinned
@@ -478,13 +477,13 @@ class HandleMenu(
R.dimen.desktop_mode_handle_menu_icon_button_ripple_inset_base)
private val iconButtonRippleRadius = context.resources.getDimensionPixelSize(
R.dimen.desktop_mode_handle_menu_icon_button_ripple_radius)
- private val iconButtonDrawableInsetsBase = DrawableInsets(t=iconButtondrawableBaseInset,
- b=iconButtondrawableBaseInset, l=iconButtondrawableBaseInset,
- r=iconButtondrawableBaseInset)
- private val iconButtonDrawableInsetsLeft = DrawableInsets(t=iconButtondrawableBaseInset,
- b=iconButtondrawableBaseInset, l=iconButtondrawableShiftInset, r=0)
- private val iconButtonDrawableInsetsRight = DrawableInsets(t=iconButtondrawableBaseInset,
- b=iconButtondrawableBaseInset, l=0, r=iconButtondrawableShiftInset)
+ private val iconButtonDrawableInsetsBase = DrawableInsets(t = iconButtondrawableBaseInset,
+ b = iconButtondrawableBaseInset, l = iconButtondrawableBaseInset,
+ r = iconButtondrawableBaseInset)
+ private val iconButtonDrawableInsetsLeft = DrawableInsets(t = iconButtondrawableBaseInset,
+ b = iconButtondrawableBaseInset, l = iconButtondrawableShiftInset, r = 0)
+ private val iconButtonDrawableInsetsRight = DrawableInsets(t = iconButtondrawableBaseInset,
+ b = iconButtondrawableBaseInset, l = 0, r = iconButtondrawableShiftInset)
// App Info Pill.
private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
@@ -702,7 +701,7 @@ class HandleMenu(
imageTintList = ColorStateList.valueOf(style.textColor)
this.taskInfo = this@HandleMenuView.taskInfo
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = iconButtonDrawableInsetsBase
@@ -740,7 +739,7 @@ class HandleMenu(
else iconButtonDrawableInsetsRight
fullscreenBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = startInsets
@@ -748,7 +747,7 @@ class HandleMenu(
}
splitscreenBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = iconButtonDrawableInsetsBase
@@ -756,7 +755,7 @@ class HandleMenu(
}
floatingBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = iconButtonDrawableInsetsBase
@@ -764,7 +763,7 @@ class HandleMenu(
}
desktopBtn.apply {
- background = createRippleDrawable(
+ background = createBackgroundDrawable(
color = style.textColor,
cornerRadius = iconButtonRippleRadius,
drawableInsets = endInsets
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 7baef2b2dc97..bde46a1bc375 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -361,6 +361,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
outResult.mRootView = rootView;
final boolean fontScaleChanged = mWindowDecorConfig != null
&& mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale;
+ final boolean localeListChanged = mWindowDecorConfig != null
+ && !mWindowDecorConfig.getLocales()
+ .equals(mTaskInfo.getConfiguration().getLocales());
final int oldDensityDpi = mWindowDecorConfig != null
? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
final int oldNightMode = mWindowDecorConfig != null
@@ -376,7 +379,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
|| oldLayoutResId != mLayoutResId
|| oldNightMode != newNightMode
|| mDecorWindowContext == null
- || fontScaleChanged) {
+ || fontScaleChanged
+ || localeListChanged) {
releaseViews(wct);
if (!obtainDisplayOrRegisterListener()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
new file mode 100644
index 000000000000..39ccf5bd03a7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.windowdecor.common
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.Context
+import android.view.WindowManager
+import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.splitscreen.SplitScreenController
+
+/**
+ * Resolves whether, given a task and its associated display that it is currently on, to show the
+ * app handle/header or not.
+ */
+class AppHandleAndHeaderVisibilityHelper (
+ private val context: Context,
+ private val displayController: DisplayController,
+ private val desktopModeCompatPolicy: DesktopModeCompatPolicy
+) {
+ var splitScreenController: SplitScreenController? = null
+
+ /**
+ * Returns, given a task's attribute and its display attribute, whether the app
+ * handle/header should show or not for this task.
+ */
+ fun shouldShowAppHandleOrHeader(taskInfo: ActivityManager.RunningTaskInfo): Boolean {
+ if (!ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue) {
+ return allowedForTask(taskInfo)
+ }
+ return allowedForTask(taskInfo) && allowedForDisplay(taskInfo.displayId)
+ }
+
+ private fun allowedForTask(taskInfo: ActivityManager.RunningTaskInfo): Boolean {
+ // TODO (b/382023296): Remove once we no longer rely on
+ // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay
+ if (displayController.getDisplay(taskInfo.displayId) == null) {
+ // If DisplayController doesn't have it tracked, it could be a private/managed display.
+ return false
+ }
+ if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) return true
+ if (splitScreenController?.isTaskRootOrStageRoot(taskInfo.taskId) == true) {
+ return false
+ }
+
+ if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) {
+ return false
+ }
+
+ // TODO (b/382023296): Remove once we no longer rely on
+ // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay
+ val isOnLargeScreen =
+ displayController.getDisplay(taskInfo.displayId).minSizeDimensionDp >=
+ WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ if (!DesktopModeStatus.canEnterDesktopMode(context)
+ && DesktopModeStatus.overridesShowAppHandle(context)
+ && !isOnLargeScreen
+ ) {
+ // Devices with multiple screens may enable the app handle but it should not show on
+ // small screens
+ return false
+ }
+ return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)
+ && !isWallpaperTask(taskInfo)
+ && taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED
+ && taskInfo.activityType == WindowConfiguration.ACTIVITY_TYPE_STANDARD
+ && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop
+ }
+
+ private fun allowedForDisplay(displayId: Int): Boolean {
+ // If DisplayController doesn't have it tracked, it could be a private/managed display.
+ val display = displayController.getDisplay(displayId)
+ if (display == null) return false
+
+ if (DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)) {
+ return true
+ }
+ // If on default display and on Large Screen (unfolded), show app handle
+ return DesktopModeStatus.overridesShowAppHandle(context)
+ && display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
index f44b15a23b90..f08cfa987cc7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -20,7 +20,6 @@ import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
-import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
@@ -48,45 +47,6 @@ fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
}
/**
- * Creates a RippleDrawable with specified color, corner radius, and insets.
- */
-fun createRippleDrawable(
- @ColorInt color: Int,
- cornerRadius: Int,
- drawableInsets: DrawableInsets,
-): RippleDrawable {
- return RippleDrawable(
- ColorStateList(
- arrayOf(
- intArrayOf(android.R.attr.state_hovered),
- intArrayOf(android.R.attr.state_pressed),
- intArrayOf(),
- ),
- intArrayOf(
- replaceColorAlpha(color, OPACITY_11),
- replaceColorAlpha(color, OPACITY_15),
- Color.TRANSPARENT,
- )
- ),
- null /* content */,
- LayerDrawable(arrayOf(
- ShapeDrawable().apply {
- shape = RoundRectShape(
- FloatArray(8) { cornerRadius.toFloat() },
- null /* inset */,
- null /* innerRadii */
- )
- paint.color = Color.WHITE
- }
- )).apply {
- require(numberOfLayers == 1) { "Must only contain one layer" }
- setLayerInset(0 /* index */,
- drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
- }
- )
-}
-
-/**
* Creates a background drawable with specified color, corner radius, and insets.
*/
fun createBackgroundDrawable(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
index 801048adda4d..957898fd0088 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
@@ -21,6 +21,7 @@ import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
+import android.os.LocaleList
import android.os.UserHandle
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting
@@ -80,6 +81,13 @@ class WindowDecorTaskResourceLoader(
*/
private val existingTasks = mutableSetOf<Int>()
+ /**
+ * A map of task -> localeList to keep track of the language of app name that's currently
+ * cached in |taskToResourceCache|.
+ */
+ @VisibleForTesting
+ val localeListOnCache = ConcurrentHashMap<Int, LocaleList>()
+
init {
shellInit.addInitCallback(this::onInit, this)
}
@@ -99,11 +107,14 @@ class WindowDecorTaskResourceLoader(
fun getName(taskInfo: RunningTaskInfo): CharSequence {
checkWindowDecorExists(taskInfo)
val cachedResources = taskToResourceCache[taskInfo.taskId]
- if (cachedResources != null) {
+ val localeListActiveOnCacheTime = localeListOnCache[taskInfo.taskId]
+ if (cachedResources != null &&
+ taskInfo.getConfiguration().getLocales().equals(localeListActiveOnCacheTime)) {
return cachedResources.appName
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.appName
}
@@ -117,6 +128,7 @@ class WindowDecorTaskResourceLoader(
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.appIcon
}
@@ -130,6 +142,7 @@ class WindowDecorTaskResourceLoader(
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.veilIcon
}
@@ -142,6 +155,7 @@ class WindowDecorTaskResourceLoader(
fun onWindowDecorClosed(taskInfo: RunningTaskInfo) {
existingTasks.remove(taskInfo.taskId)
taskToResourceCache.remove(taskInfo.taskId)
+ localeListOnCache.remove(taskInfo.taskId)
}
private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 598a101b8bcd..597e4a55ed0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -59,6 +59,7 @@ import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.sysui.ShellController;
@@ -738,6 +739,22 @@ public class CompatUIControllerTest extends ShellTestCase {
verify(mController, never()).removeLayouts(taskInfo.taskId);
}
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
+ public void testSendCompatUIRequest_createRestartDialog() {
+ TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false);
+ doReturn(true).when(mMockRestartDialogLayout)
+ .needsToBeRecreated(any(TaskInfo.class),
+ any(ShellTaskOrganizer.TaskListener.class));
+ doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled();
+ doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo));
+
+ mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog(
+ taskInfo, mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
+ }
+
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
/* isFocused */ false, /* isTopActivityTransparent */ false);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index e9f92cfd7c56..0c585b3e843a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -431,6 +431,38 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+ Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX,
+ )
+ fun startAndAnimateLaunchTransition_withMinimizeChange_wrongTaskId_reparentsMinimizeChange() {
+ val wct = WindowContainerTransaction()
+ val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val launchTaskChange = createChange(launchingTask, mode = TRANSIT_OPEN)
+ val minimizeChange = createChange(minimizingTask)
+ val transition = Binder()
+ whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+ .thenReturn(transition)
+
+ mixedHandler.startLaunchTransition(
+ transitionType = TRANSIT_OPEN,
+ wct = wct,
+ taskId = Int.MAX_VALUE,
+ minimizingTaskId = minimizingTask.taskId,
+ )
+ mixedHandler.startAnimation(
+ transition,
+ createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)),
+ SurfaceControl.Transaction(),
+ SurfaceControl.Transaction(),
+ ) {}
+
+ verify(rootTaskDisplayAreaOrganizer)
+ .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any())
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() {
val wct = WindowContainerTransaction()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt
index fbc940663d19..6a99d4770728 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeMoveToDisplayTransitionHandlerTest.kt
@@ -16,8 +16,10 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
import android.view.WindowManager
import android.window.TransitionInfo
import androidx.test.filters.SmallTest
@@ -30,6 +32,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
@SmallTest
@RunWithLooper
@@ -55,7 +59,9 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
info =
TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
addChange(
- TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 1) }
+ TransitionInfo.Change(mock(), mock()).apply {
+ setDisplayId(/* start= */ 1, /* end= */ 1)
+ }
)
},
startTransaction = StubTransaction(),
@@ -74,7 +80,9 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
info =
TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
addChange(
- TransitionInfo.Change(mock(), mock()).apply { setDisplayId(1, 2) }
+ TransitionInfo.Change(mock(), mock()).apply {
+ setDisplayId(/* start= */ 1, /* end= */ 2)
+ }
)
},
startTransaction = StubTransaction(),
@@ -84,4 +92,77 @@ class DesktopModeMoveToDisplayTransitionHandlerTest : ShellTestCase() {
assertTrue("Should animate display change transition", animates)
}
+
+ @Test
+ fun startAnimation_movingActivityEmbedding_shouldSetCorrectBounds() {
+ val leashLeft = mock<SurfaceControl>()
+ val leashRight = mock<SurfaceControl>()
+ val leashContainer = mock<SurfaceControl>()
+ val startTransaction = spy(StubTransaction())
+
+ handler.startAnimation(
+ transition = mock(),
+ info =
+ TransitionInfo(WindowManager.TRANSIT_CHANGE, /* flags= */ 0).apply {
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ flags = TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY
+ leash = leashLeft
+ setDisplayId(/* start= */ 1, /* end= */ 2)
+ setEndAbsBounds(
+ Rect(
+ /* left= */ 100,
+ /* top= */ 100,
+ /* right= */ 500,
+ /* bottom= */ 700,
+ )
+ )
+ setEndRelOffset(/* left= */ 0, /* top= */ 0)
+ }
+ )
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ flags = TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY
+ leash = leashRight
+ setDisplayId(1, 2)
+ setEndAbsBounds(
+ Rect(
+ /* left= */ 500,
+ /* top= */ 100,
+ /* right= */ 900,
+ /* bottom= */ 700,
+ )
+ )
+ setEndRelOffset(/* left= */ 400, /* top= */ 0)
+ }
+ )
+ addChange(
+ TransitionInfo.Change(mock(), mock()).apply {
+ flags = TransitionInfo.FLAG_TRANSLUCENT
+ leash = leashContainer
+ setDisplayId(/* start= */ 1, /* end= */ 2)
+ setEndAbsBounds(
+ Rect(
+ /* left= */ 100,
+ /* top= */ 100,
+ /* right= */ 900,
+ /* bottom= */ 700,
+ )
+ )
+ setEndRelOffset(/* left= */ 100, /* top= */ 100)
+ }
+ )
+ },
+ startTransaction = startTransaction,
+ finishTransaction = StubTransaction(),
+ finishCallback = mock(),
+ )
+
+ verify(startTransaction).setPosition(leashLeft, 0f, 0f)
+ verify(startTransaction).setPosition(leashRight, 400f, 0f)
+ verify(startTransaction).setPosition(leashContainer, 100f, 100f)
+ verify(startTransaction).setWindowCrop(leashLeft, 400, 600)
+ verify(startTransaction).setWindowCrop(leashRight, 400, 600)
+ verify(startTransaction).setWindowCrop(leashContainer, 800, 600)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index d093629000f3..75308442d76a 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
@@ -31,6 +31,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ActivityInfo
import android.content.pm.ActivityInfo.CONFIG_DENSITY
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
@@ -1133,6 +1134,54 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
+ fun addMoveToDeskTaskChanges_newTaskInstance_inheritsClosingInstanceBounds() {
+ // Setup existing task.
+ val existingTask = setUpFreeformTask(active = true)
+ val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
+ existingTask.topActivity = testComponent
+ existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
+ // Set up new instance of already existing task.
+ val launchingTask = setUpFullscreenTask()
+ launchingTask.topActivity = testComponent
+ launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
+
+ // Move new instance to desktop. By default multi instance is not supported so first
+ // instance will close.
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDeskTaskChanges(wct, launchingTask, deskId = 0)
+
+ // New instance should inherit task bounds of old instance.
+ assertThat(findBoundsChange(wct, launchingTask))
+ .isEqualTo(existingTask.configuration.windowConfiguration.bounds)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
+ fun handleRequest_newTaskInstance_inheritsClosingInstanceBounds() {
+ setUpLandscapeDisplay()
+ // Setup existing task.
+ val existingTask = setUpFreeformTask(active = true)
+ val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
+ existingTask.topActivity = testComponent
+ existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
+ // Set up new instance of already existing task.
+ val launchingTask = setUpFreeformTask(active = false)
+ taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId)
+ launchingTask.topActivity = testComponent
+ launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
+
+ // Move new instance to desktop. By default multi instance is not supported so first
+ // instance will close.
+ val wct = controller.handleRequest(Binder(), createTransition(launchingTask))
+
+ assertNotNull(wct, "should handle request")
+ val finalBounds = findBoundsChange(wct, launchingTask)
+ // New instance should inherit task bounds of old instance.
+ assertThat(finalBounds).isEqualTo(existingTask.configuration.windowConfiguration.bounds)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
fun handleRequest_newFreeformTaskLaunch_cascadeApplied() {
setUpLandscapeDisplay()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 0871d38ceb46..6e7adf368155 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
import com.android.internal.jank.InteractionJankMonitor
import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
@@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.MockitoSession
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
.strictness(Strictness.LENIENT)
.mockStatic(SystemProperties::class.java)
.startMocking()
+ whenever(
+ transitions.startTransition(
+ eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP),
+ /* wct= */ any(),
+ eq(defaultHandler),
+ )
+ )
+ .thenReturn(mock<IBinder>())
}
@After
@@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
val startTransition = startDrag(defaultHandler, task)
val endTransition = mock<IBinder>()
defaultHandler.onTaskResizeAnimationListener = mock()
- defaultHandler.mergeAnimation(
+ mergeAnimation(
transition = endTransition,
- info =
- createTransitionInfo(
- type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
- draggedTask = task,
- ),
- startT = mock<SurfaceControl.Transaction>(),
- finishT = mock<SurfaceControl.Transaction>(),
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ task = task,
mergeTarget = startTransition,
- finishCallback = mock<Transitions.TransitionFinishCallback>(),
)
defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock())
@@ -701,6 +706,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() {
+ val finishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback)
+
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ verify(finishCallback, never()).onTransitionFinished(anyOrNull())
+ verify(dragAnimator, never()).cancelAnimator()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() {
+ val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ val startTransition =
+ startDrag(defaultHandler, task, finishCallback = startDragFinishCallback)
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeAnimation(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ task = task,
+ mergeTarget = startTransition,
+ )
+
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() {
+ val task1 = createTask()
+ val startTransition = startDrag(defaultHandler, task1)
+ val endTransition =
+ defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction())
+ val startTransaction = mock<SurfaceControl.Transaction>()
+ val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>()
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ val didAnimate =
+ defaultHandler.startAnimation(
+ transition = requireNotNull(endTransition),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task1,
+ ),
+ startTransaction = startTransaction,
+ finishTransaction = mock(),
+ finishCallback = endDragFinishCallback,
+ )
+
+ assertThat(didAnimate).isTrue()
+ verify(startTransaction).apply()
+ verify(endDragFinishCallback).onTransitionFinished(anyOrNull())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() {
+ val task1 = createTask()
+ val startTransition = startDrag(defaultHandler, task1)
+ verify(dragAnimator).startAnimation()
+ val endTransition =
+ defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction())
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeInterruptingTransition(mergeTarget = startTransition)
+ defaultHandler.startAnimation(
+ transition = requireNotNull(endTransition),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task1,
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = mock(),
+ )
+
+ startDrag(defaultHandler, createTask())
+
+ verify(dragAnimator, times(2)).startAnimation()
+ }
+
+ private fun mergeInterruptingTransition(mergeTarget: IBinder) {
+ defaultHandler.mergeAnimation(
+ transition = mock<IBinder>(),
+ info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()),
+ startT = mock(),
+ finishT = mock(),
+ mergeTarget = mergeTarget,
+ finishCallback = mock(),
+ )
+ }
+
+ private fun mergeAnimation(
+ transition: IBinder = mock(),
+ type: Int,
+ mergeTarget: IBinder,
+ task: RunningTaskInfo,
+ ) {
+ defaultHandler.mergeAnimation(
+ transition = transition,
+ info = createTransitionInfo(type = type, draggedTask = task),
+ startT = mock(),
+ finishT = mock(),
+ mergeTarget = mergeTarget,
+ finishCallback = mock(),
+ )
+ }
+
+ @Test
fun getAnimationFraction_returnsFraction() {
val fraction =
SpringDragToDesktopTransitionHandler.getAnimationFraction(
@@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
finishTransaction: SurfaceControl.Transaction = mock(),
homeChange: TransitionInfo.Change? = createHomeChange(),
transitionRootLeash: SurfaceControl = mock(),
+ finishCallback: Transitions.TransitionFinishCallback = mock(),
): IBinder {
whenever(dragAnimator.position).thenReturn(PointF())
// Simulate transition is started and is ready to animate.
@@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
),
startTransaction = startTransaction,
finishTransaction = finishTransaction,
- finishCallback = {},
+ finishCallback = finishCallback,
)
return transition
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index 6ecebd76a951..75f6bda4d750 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -203,7 +203,7 @@ class GroupedTaskInfoTest : ShellTestCase() {
assertThat(taskInfoFromParcel.taskInfoList).hasSize(3)
// Only compare task ids
val taskIdComparator = Correspondence.transforming<TaskInfo, Int>(
- { it?.taskId }, "has taskId of"
+ { it.taskId }, "has taskId of"
)
assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
.containsExactly(1, 2, 3).inOrder()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index fb62ba75e056..edf91fe62e7d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -234,14 +234,25 @@ class DesktopModeStatusTest : ShellTestCase() {
assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
+ @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE)
@Test
fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() {
doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
- doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
+ doReturn(false).whenever(mockResources)
+ .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
+ @EnableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE)
+ @Test
+ fun isPDDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsTrue() {
+ doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+ doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
+
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+ }
+
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
index 257bbb5603a7..b07b6c1a3a87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
@@ -22,6 +22,7 @@ import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.SurfaceControl
+import android.window.TaskSnapshot
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags
import com.android.wm.shell.MockToken
@@ -33,6 +34,7 @@ import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.google.common.truth.Truth.assertThat
import org.junit.After
+import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -84,7 +86,29 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() {
assertThat(menu.menuViewContainer).isInstanceOf(AdditionalSystemViewContainer::class.java)
}
- private fun createMenu(task: RunningTaskInfo) = DesktopHeaderManageWindowsMenu(
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testShow_nullSnapshotDoesNotCauseNPE() {
+ val task = createFreeformTask()
+ val snapshotList = listOf(Pair(/* index = */ 1, /* snapshot = */ null))
+ // Set as immersive so that menu is created as system view container (simpler of the
+ // options)
+ userRepositories.getProfile(DEFAULT_USER_ID).setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = true
+ )
+ try {
+ menu = createMenu(task, snapshotList)
+ } catch (e: NullPointerException) {
+ fail("Null snapshot should not have thrown null pointer exception")
+ }
+ }
+
+ private fun createMenu(
+ task: RunningTaskInfo,
+ snapshotList: List<Pair<Int, TaskSnapshot?>> = emptyList()
+ ) = DesktopHeaderManageWindowsMenu(
callerTaskInfo = task,
x = 0,
y = 0,
@@ -94,7 +118,7 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() {
desktopUserRepositories = userRepositories,
surfaceControlBuilderSupplier = { SurfaceControl.Builder() },
surfaceControlTransactionSupplier = { SurfaceControl.Transaction() },
- snapshotList = emptyList(),
+ snapshotList = snapshotList,
onIconClickListener = {},
onOutsideClickListener = {},
)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index e89a122595d5..d69509faf4ec 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -115,7 +115,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
.spyStatic(DragPositioningCallbackUtility::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(Mockito.any(),
+ Mockito.any()) }
doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) }
doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) }
@@ -394,7 +395,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
- doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any()) }
setUpMockDecorationsForTasks(task)
onTaskOpening(task)
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 81dfaed56b6f..a1f40fdefee9 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
@@ -79,6 +79,7 @@ import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.util.StubTransaction
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier
@@ -174,6 +175,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel
protected lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
+ protected lateinit var appHandleAndHeaderVisibilityHelper: AppHandleAndHeaderVisibilityHelper
fun setUpCommon() {
spyContext = spy(mContext)
@@ -185,9 +187,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository)
whenever(mockDisplayController.getDisplayContext(any())).thenReturn(spyContext)
whenever(mockDisplayController.getDisplay(any())).thenReturn(display)
+ whenever(display.type).thenReturn(Display.TYPE_INTERNAL)
whenever(mockDesktopUserRepositories.getProfile(anyInt()))
.thenReturn(mockDesktopRepository)
desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext)
+ appHandleAndHeaderVisibilityHelper =
+ AppHandleAndHeaderVisibilityHelper(spyContext, mockDisplayController,
+ desktopModeCompatPolicy)
desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
spyContext,
testShellExecutor,
@@ -222,6 +228,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
Optional.of(mockTasksLimiter),
mockAppHandleEducationController,
mockAppToWebEducationController,
+ appHandleAndHeaderVisibilityHelper,
mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
mockTaskPositionerFactory,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index a2927fa3527b..9a2e2fad50be 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -60,6 +60,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
+import android.os.LocaleList;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.AttachedSurfaceControl;
@@ -97,6 +98,7 @@ import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.function.Supplier;
/**
@@ -475,6 +477,50 @@ public class WindowDecorationTests extends ShellTestCase {
}
@Test
+ public void testReinflateViewsOnLocaleListChange() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US));
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+
+ final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo2.configuration.setLocales(new LocaleList(Locale.US, Locale.FRANCE));
+ windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should be called since the locale list has changed.
+ verify(windowDecor, times(1)).releaseViews(any());
+ }
+
+ @Test
+ public void testViewNotReinflatedWhenLocaleListNotChanged() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US));
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should not be called since nothing has changed.
+ verify(windowDecor, never()).releaseViews(any());
+ }
+
+ @Test
public void testLayoutResultCalculation_fullWidthCaption() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
index c61e0eb3b5af..c8ccac35d4c4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
@@ -23,6 +23,7 @@ import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
+import android.os.LocaleList
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableContext
@@ -39,6 +40,7 @@ import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader.AppResources
import com.google.common.truth.Truth.assertThat
+import java.util.Locale
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -116,8 +118,10 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
@Test
fun testGetName_cached_returnsFromCache() {
val task = createTaskInfo(context.userId)
+ task.configuration.setLocales(LocaleList(Locale.US))
loader.onWindowDecorCreated(task)
loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock())
+ loader.localeListOnCache[task.taskId] = LocaleList(Locale.US)
loader.getName(task)
@@ -130,6 +134,19 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
}
@Test
+ fun testGetName_cached_localesChanged_loadsResourceAndCaches() {
+ val task = createTaskInfo(context.userId)
+ loader.onWindowDecorCreated(task)
+ loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock())
+ loader.localeListOnCache[task.taskId] = LocaleList(Locale.US, Locale.FRANCE)
+ task.configuration.setLocales(LocaleList(Locale.FRANCE, Locale.US))
+ doReturn("App Name but in French").whenever(mockPackageManager).getApplicationLabel(any())
+
+ assertThat(loader.getName(task)).isEqualTo("App Name but in French")
+ assertThat(loader.taskToResourceCache[task.taskId]?.appName).isEqualTo("App Name but in French")
+ }
+
+ @Test
fun testGetHeaderIcon_notCached_loadsResourceAndCaches() {
val task = createTaskInfo(context.userId)
loader.onWindowDecorCreated(task)
diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp
index 7e14b88a47fa..ef5406250251 100644
--- a/libs/hostgraphics/HostBufferQueue.cpp
+++ b/libs/hostgraphics/HostBufferQueue.cpp
@@ -29,6 +29,7 @@ public:
}
virtual status_t detachBuffer(int slot) {
+ mBuffer.clear();
return OK;
}
diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig
index 83b1778fd611..f8eb41826c6f 100644
--- a/location/java/android/location/flags/location.aconfig
+++ b/location/java/android/location/flags/location.aconfig
@@ -179,3 +179,10 @@ flag {
}
}
+flag {
+ name: "gnss_location_provider_overlay_2025_devices"
+ namespace: "location"
+ description: "Flag for GNSS location provider overlay for 2025 devices"
+ bug: "398254728"
+ is_fixed_read_only: true
+}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index d082c7384fd1..32af7c6fca68 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -4857,7 +4857,7 @@ public class AudioManager {
focusReceiver = addClientIdToFocusReceiverLocked(clientFakeId);
}
- return handleExternalAudioPolicyWaitIfNeeded(clientFakeId, focusReceiver);
+ return handleExternalAudioPolicyWaitIfNeeded(clientFakeId, focusReceiver, afr);
}
/**
@@ -5070,7 +5070,7 @@ public class AudioManager {
focusReceiver = addClientIdToFocusReceiverLocked(clientId);
}
- return handleExternalAudioPolicyWaitIfNeeded(clientId, focusReceiver);
+ return handleExternalAudioPolicyWaitIfNeeded(clientId, focusReceiver, afr);
}
@GuardedBy("mFocusRequestsLock")
@@ -5086,11 +5086,20 @@ public class AudioManager {
}
private @FocusRequestResult int handleExternalAudioPolicyWaitIfNeeded(String clientId,
- BlockingFocusResultReceiver focusReceiver) {
+ BlockingFocusResultReceiver focusReceiver, @NonNull AudioFocusRequest afr) {
focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS);
- if (DEBUG && !focusReceiver.receivedResult()) {
- Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded"
- + " response from ext policy timed out, denying request");
+ if (!focusReceiver.receivedResult()) {
+ if (DEBUG) {
+ Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded"
+ + " response from ext policy timed out, denying request");
+ }
+ try {
+ // To prevent from orphan focus holder, cleanup
+ abandonAudioFocus(afr.getOnAudioFocusChangeListener());
+ } catch (Exception e) {
+ Log.e(TAG, "handleExternalAudioPolicyWaitIfNeeded failed to abandon audio"
+ +" focus after time out, error: " + e.getMessage());
+ }
}
synchronized (mFocusRequestsLock) {
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 4e86eacea404..f3b21bfdaa3c 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -3836,6 +3836,151 @@ public final class MediaCodecInfo {
maxBlocks, maxBlocksPerSecond,
blockSize, blockSize,
1 /* widthAlignment */, 1 /* heightAlignment */);
+ } else if (GetFlag(() -> android.media.codec.Flags.apvSupport())
+ && mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_APV)) {
+ maxBlocksPerSecond = 11880;
+ maxBps = 7000000;
+
+ // Sample rate, and Bit rate for APV Codec,
+ // corresponding to the definitions in
+ // "10.1.4. Levels and bands"
+ // found at https://www.ietf.org/archive/id/draft-lim-apv-03.html
+ for (CodecProfileLevel profileLevel: profileLevels) {
+ long SR = 0; // luma sample rate
+ int BR = 0; // bit rate bps
+ switch (profileLevel.level) {
+ case CodecProfileLevel.APVLevel1Band0:
+ SR = 3041280; BR = 7000000; break;
+ case CodecProfileLevel.APVLevel1Band1:
+ SR = 3041280; BR = 11000000; break;
+ case CodecProfileLevel.APVLevel1Band2:
+ SR = 3041280; BR = 14000000; break;
+ case CodecProfileLevel.APVLevel1Band3:
+ SR = 3041280; BR = 21000000; break;
+ case CodecProfileLevel.APVLevel11Band0:
+ SR = 6082560; BR = 14000000; break;
+ case CodecProfileLevel.APVLevel11Band1:
+ SR = 6082560; BR = 21000000; break;
+ case CodecProfileLevel.APVLevel11Band2:
+ SR = 6082560; BR = 28000000; break;
+ case CodecProfileLevel.APVLevel11Band3:
+ SR = 6082560; BR = 42000000; break;
+ case CodecProfileLevel.APVLevel2Band0:
+ SR = 15667200; BR = 36000000; break;
+ case CodecProfileLevel.APVLevel2Band1:
+ SR = 15667200; BR = 53000000; break;
+ case CodecProfileLevel.APVLevel2Band2:
+ SR = 15667200; BR = 71000000; break;
+ case CodecProfileLevel.APVLevel2Band3:
+ SR = 15667200; BR = 106000000; break;
+ case CodecProfileLevel.APVLevel21Band0:
+ SR = 31334400; BR = 71000000; break;
+ case CodecProfileLevel.APVLevel21Band1:
+ SR = 31334400; BR = 106000000; break;
+ case CodecProfileLevel.APVLevel21Band2:
+ SR = 31334400; BR = 141000000; break;
+ case CodecProfileLevel.APVLevel21Band3:
+ SR = 31334400; BR = 212000000; break;
+ case CodecProfileLevel.APVLevel3Band0:
+ SR = 66846720; BR = 101000000; break;
+ case CodecProfileLevel.APVLevel3Band1:
+ SR = 66846720; BR = 151000000; break;
+ case CodecProfileLevel.APVLevel3Band2:
+ SR = 66846720; BR = 201000000; break;
+ case CodecProfileLevel.APVLevel3Band3:
+ SR = 66846720; BR = 301000000; break;
+ case CodecProfileLevel.APVLevel31Band0:
+ SR = 133693440; BR = 201000000; break;
+ case CodecProfileLevel.APVLevel31Band1:
+ SR = 133693440; BR = 301000000; break;
+ case CodecProfileLevel.APVLevel31Band2:
+ SR = 133693440; BR = 401000000; break;
+ case CodecProfileLevel.APVLevel31Band3:
+ SR = 133693440; BR = 602000000; break;
+ case CodecProfileLevel.APVLevel4Band0:
+ SR = 265420800; BR = 401000000; break;
+ case CodecProfileLevel.APVLevel4Band1:
+ SR = 265420800; BR = 602000000; break;
+ case CodecProfileLevel.APVLevel4Band2:
+ SR = 265420800; BR = 780000000; break;
+ case CodecProfileLevel.APVLevel4Band3:
+ SR = 265420800; BR = 1170000000; break;
+ case CodecProfileLevel.APVLevel41Band0:
+ SR = 530841600; BR = 780000000; break;
+ case CodecProfileLevel.APVLevel41Band1:
+ SR = 530841600; BR = 1170000000; break;
+ case CodecProfileLevel.APVLevel41Band2:
+ SR = 530841600; BR = 1560000000; break;
+ case CodecProfileLevel.APVLevel41Band3:
+ // Current API allows bitrates only up to Max Integer
+ // Hence we are limiting internal limits to Integer.MAX_VALUE
+ // even when actual Level/Band limits are higher
+ SR = 530841600; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band0:
+ SR = 1061683200; BR = 1560000000; break;
+ case CodecProfileLevel.APVLevel5Band1:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band2:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band3:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel51Band0:
+ case CodecProfileLevel.APVLevel51Band1:
+ case CodecProfileLevel.APVLevel51Band2:
+ case CodecProfileLevel.APVLevel51Band3:
+ SR = 2123366400; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel6Band0:
+ case CodecProfileLevel.APVLevel6Band1:
+ case CodecProfileLevel.APVLevel6Band2:
+ case CodecProfileLevel.APVLevel6Band3:
+ SR = 4777574400L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel61Band0:
+ case CodecProfileLevel.APVLevel61Band1:
+ case CodecProfileLevel.APVLevel61Band2:
+ case CodecProfileLevel.APVLevel61Band3:
+ SR = 8493465600L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel7Band0:
+ case CodecProfileLevel.APVLevel7Band1:
+ case CodecProfileLevel.APVLevel7Band2:
+ case CodecProfileLevel.APVLevel7Band3:
+ SR = 16986931200L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel71Band0:
+ case CodecProfileLevel.APVLevel71Band1:
+ case CodecProfileLevel.APVLevel71Band2:
+ case CodecProfileLevel.APVLevel71Band3:
+ SR = 33973862400L; BR = Integer.MAX_VALUE; break;
+ default:
+ Log.w(TAG, "Unrecognized level "
+ + profileLevel.level + " for " + mime);
+ errors |= ERROR_UNRECOGNIZED;
+ }
+ switch (profileLevel.profile) {
+ case CodecProfileLevel.APVProfile422_10:
+ case CodecProfileLevel.APVProfile422_10HDR10:
+ case CodecProfileLevel.APVProfile422_10HDR10Plus:
+ break;
+ default:
+ Log.w(TAG, "Unrecognized profile "
+ + profileLevel.profile + " for " + mime);
+ errors |= ERROR_UNRECOGNIZED;
+ }
+ errors &= ~ERROR_NONE_SUPPORTED;
+ maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond);
+ maxBps = Math.max(BR, maxBps);
+ }
+
+ final int blockSize = 16;
+ maxBlocks = Integer.MAX_VALUE;
+ maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize);
+ maxBlocks = (int) Math.min((long) maxBlocks, maxBlocksPerSecond);
+ // Max frame size in APV is 2^24
+ int maxLengthInBlocks = Utils.divUp((int) Math.pow(2, 24), blockSize);
+ maxLengthInBlocks = Math.min(maxLengthInBlocks, maxBlocks);
+ applyMacroBlockLimits(
+ maxLengthInBlocks, maxLengthInBlocks,
+ maxBlocks, maxBlocksPerSecond,
+ blockSize, blockSize,
+ 2 /* widthAlignment */, 1 /* heightAlignment */);
} else {
Log.w(TAG, "Unsupported mime " + mime);
// using minimal bitrate here. should be overriden by
diff --git a/media/java/android/media/tv/extension/scan/IScanListener.aidl b/media/java/android/media/tv/extension/scan/IScanListener.aidl
index 2c4807f97c58..79810a7b035d 100644
--- a/media/java/android/media/tv/extension/scan/IScanListener.aidl
+++ b/media/java/android/media/tv/extension/scan/IScanListener.aidl
@@ -27,7 +27,7 @@ oneway interface IScanListener {
// notify the scan progress.
void onScanProgress(String scanProgress, in Bundle scanProgressInfo);
// notify the scan completion.
- void onScanCompleted(int scanResult);
+ void onScanCompleted(int scanResult, in Bundle optionScanInfo);
// notify that the temporaily held channel list is stored.
void onStoreCompleted(int storeResult);
}
diff --git a/media/java/android/media/tv/extension/scan/IScanSession.aidl b/media/java/android/media/tv/extension/scan/IScanSession.aidl
index d42eca1342b5..f53b09661ba6 100644
--- a/media/java/android/media/tv/extension/scan/IScanSession.aidl
+++ b/media/java/android/media/tv/extension/scan/IScanSession.aidl
@@ -24,7 +24,7 @@ import android.os.Bundle;
interface IScanSession {
// Start a service scan.
int startScan(int broadcastType, String countryCode, String operator, in int[] frequency,
- String scanType, String languageCode);
+ String scanType, String languageCode, in Bundle optionalScanParams);
// Reset the scan information held in TIS.
int resetScan();
// Cancel scan.
diff --git a/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl b/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl
index 1b1577ffc015..0e9ac5f04e5d 100644
--- a/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl
+++ b/media/java/android/media/tv/extension/servicedb/IServiceListEdit.aidl
@@ -78,4 +78,11 @@ interface IServiceListEdit {
int addPredefinedChannelList(String serviceListId, in Bundle[] predefinedListBundle);
// Add predefined satellite info of Hotbird 13E in scan two satellite scene EU region.
int addPredefinedSatInfo(String serviceListId, in Bundle predefinedSatInfoBundle);
+
+ // Get the logo URI for a specific service - DVB-I only.
+ String getServiceLogoUri(int serviceRecordId);
+ // Get the installed service list information for a specific channel list id - DVB-I only.
+ Bundle getInstalledServiceListInfo(String channelListId);
+ // Get all installed service list information - DVB-I only.
+ Bundle[] getAllInstalledServiceListInfo();
}
diff --git a/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl b/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl
index abd8320df11d..fced45b11d3b 100644
--- a/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl
+++ b/media/java/android/media/tv/extension/servicedb/IServiceListImportListener.aidl
@@ -16,10 +16,12 @@
package android.media.tv.extension.servicedb;
+import android.os.Bundle;
+
/**
* @hide
*/
interface IServiceListImportListener {
void onImported(int importResult);
- void onPreloaded(int preloadResult);
+ void onPreloaded(int preloadResult, in Bundle serviceListInfo);
} \ No newline at end of file
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 1e6a7b7f2810..45b746d254e1 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -971,7 +971,7 @@ void FMQWrapper::writeBuffer<HalChannelMessageContents::workDuration>(hal::WorkD
.timeStampNanos = (i == count - 1) ? now : message.timeStampNanos,
.data = HalChannelMessageContents::make<HalChannelMessageContents::workDuration,
hal::WorkDurationFixedV1>({
- .durationNanos = message.cpuDurationNanos,
+ .durationNanos = message.durationNanos,
.workPeriodStartTimestampNanos = message.workPeriodStartTimestampNanos,
.cpuDurationNanos = message.cpuDurationNanos,
.gpuDurationNanos = message.gpuDurationNanos,
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 0c64fa7d34dd..d55bbb3bbdbf 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -447,6 +447,10 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio));
}
+ public boolean isAnimatable() {
+ return mIsAnimatable;
+ }
+
private void startAnimation(Drawable drawable) {
if (!(drawable instanceof Animatable)) {
return;
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index 0f6a2a082e0c..1170f1e7c695 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -19,6 +19,7 @@ package com.android.settingslib.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
import android.widget.Spinner;
@@ -110,7 +111,6 @@ public class SettingsSpinnerPreference extends Preference
notifyChanged();
}
-
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
@@ -119,6 +119,18 @@ public class SettingsSpinnerPreference extends Preference
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
spinner.setLongClickable(false);
+ spinner.setAccessibilityDelegate(
+ new View.AccessibilityDelegate() {
+ @Override
+ public void sendAccessibilityEvent(View host, int eventType) {
+ if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
+ // Ignore the INTERRUPT events TYPE_VIEW_SELECTED or Talkback will speak
+ // for it while fragment updating.
+ return;
+ }
+ super.sendAccessibilityEvent(host, eventType);
+ }
+ });
if (mShouldPerformClick) {
mShouldPerformClick = false;
// To show dropdown view.
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
index 3d16d6f1cd56..c387d48b33da 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java
@@ -507,4 +507,25 @@ public class MediaDeviceTest {
assertThat(mPhoneMediaDevice.getSelectionBehavior()).isEqualTo(
SELECTION_BEHAVIOR_TRANSFER);
}
+
+ @Test
+ public void getSelectionBehavior_withRouteListingPreferenceItem_returnPreferenceBehavior() {
+ mItem =
+ new RouteListingPreference.Item.Builder(DEVICE_ADDRESS_1)
+ .setSelectionBehavior(SELECTION_BEHAVIOR_GO_TO_APP)
+ .build();
+ MediaDevice castMediaDevice = new ComplexMediaDevice(mContext, mRouteInfo1, mItem);
+
+ assertThat(castMediaDevice.hasRouteListingPreferenceItem()).isTrue();
+ assertThat(castMediaDevice.getSelectionBehavior()).isEqualTo(SELECTION_BEHAVIOR_GO_TO_APP);
+ }
+
+ @Test
+ public void getSelectionBehavior_withoutRouteListingPreferenceItem_returnTransfer() {
+ MediaDevice castMediaDevice =
+ new ComplexMediaDevice(mContext, mRouteInfo1, /* item= */ null);
+
+ assertThat(castMediaDevice.hasRouteListingPreferenceItem()).isFalse();
+ assertThat(castMediaDevice.getSelectionBehavior()).isEqualTo(SELECTION_BEHAVIOR_TRANSFER);
+ }
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index f0a0483aa17c..a2291123e192 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -230,6 +230,7 @@ public class SecureSettings {
Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED,
Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED,
+ Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ENABLED,
@@ -267,6 +268,7 @@ public class SecureSettings {
Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED,
Settings.Secure.HUB_MODE_TUTORIAL_STATE,
Settings.Secure.GLANCEABLE_HUB_ENABLED,
+ Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB,
Settings.Secure.STYLUS_BUTTONS_ENABLED,
Settings.Secure.STYLUS_HANDWRITING_ENABLED,
Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index b5de7e719043..a4325344709a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -324,6 +324,10 @@ public class SecureSettingsValidators {
Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL));
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
+ new InclusiveIntegerRangeValidator(
+ Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS,
+ Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE));
VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(
Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
@@ -430,6 +434,8 @@ public class SecureSettingsValidators {
VALIDATORS.put(Secure.DND_CONFIGS_MIGRATED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.HUB_MODE_TUTORIAL_STATE, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.GLANCEABLE_HUB_ENABLED, new InclusiveIntegerRangeValidator(0, 1));
+ VALIDATORS.put(Secure.WHEN_TO_START_GLANCEABLE_HUB,
+ new InclusiveIntegerRangeValidator(0, 3));
VALIDATORS.put(Secure.STYLUS_BUTTONS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.STYLUS_HANDWRITING_ENABLED,
new DiscreteValueValidator(new String[] {"-1", "0", "1"}));
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index e07832eea65e..57facdaa388c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1881,6 +1881,10 @@ class SettingsProtoDumpUtil {
SecureSettingsProto.Accessibility
.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED);
dumpSetting(s, p,
+ Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
+ SecureSettingsProto.Accessibility
+ .ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE);
+ dumpSetting(s, p,
Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED,
SecureSettingsProto.Accessibility
.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED);
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
index d08d859ec0d7..fc4d53af4b53 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
@@ -22,7 +22,6 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -32,7 +31,6 @@ import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.modifier.ModifierLocalModifierNode
import androidx.compose.ui.node.DrawModifierNode
@@ -50,11 +48,7 @@ import androidx.compose.ui.util.fastForEach
* The elements redirected to this container will be drawn above the content of this composable.
*/
fun Modifier.container(state: ContainerState): Modifier {
- return onPlaced { state.lastOffsetInWindow = it.positionInWindow() }
- .drawWithContent {
- drawContent()
- state.drawInOverlay(this)
- }
+ return this then ContainerElement(state)
}
/**
@@ -105,6 +99,30 @@ internal interface LayerRenderer {
fun drawInOverlay(drawScope: DrawScope)
}
+private data class ContainerElement(private val state: ContainerState) :
+ ModifierNodeElement<ContainerNode>() {
+ override fun create(): ContainerNode {
+ return ContainerNode(state)
+ }
+
+ override fun update(node: ContainerNode) {
+ node.state = state
+ }
+}
+
+/** A node implementing [container] that can be delegated to. */
+class ContainerNode(var state: ContainerState) :
+ Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode {
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ state.lastOffsetInWindow = coordinates.positionInWindow()
+ }
+
+ override fun ContentDrawScope.draw() {
+ drawContent()
+ state.drawInOverlay(this)
+ }
+}
+
private data class DrawInContainerElement(
var state: ContainerState,
var enabled: () -> Boolean,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 061fdd99eb1b..0a711487ccb1 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene(
modifier = Modifier.padding(horizontal = 16.dp),
)
}
- else -> CollapsedShadeHeader(viewModel = headerViewModel)
+ else ->
+ CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false)
}
Spacer(modifier = Modifier.height(16.dp))
// This view has its own horizontal padding
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index a0216268308c..8f0fb20cef36 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -52,6 +52,7 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.rememberViewModel
@@ -257,15 +258,18 @@ fun ContentScope.QuickSettingsLayout(
modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding),
)
- BrightnessSliderContainer(
- viewModel = viewModel.brightnessSliderViewModel,
- containerColor = OverlayShade.Colors.PanelBackground,
- modifier =
- Modifier.systemGestureExclusionInShade(
- enabled = { layoutState.transitionState is TransitionState.Idle }
- )
- .fillMaxWidth(),
- )
+ Box(
+ Modifier.systemGestureExclusionInShade(
+ enabled = { layoutState.transitionState is TransitionState.Idle }
+ )
+ ) {
+ BrightnessSliderContainer(
+ viewModel = viewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors.singleColor(OverlayShade.Colors.PanelBackground),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
Box {
GridAnchor()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 23baeacd76ec..86c8fc34a63c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -127,6 +127,7 @@ object ShadeHeader {
@Composable
fun ContentScope.CollapsedShadeHeader(
viewModel: ShadeHeaderViewModel,
+ isSplitShade: Boolean,
modifier: Modifier = Modifier,
) {
val cutoutLocation = LocalDisplayCutout.current.location
@@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader(
}
}
- val isShadeLayoutWide = viewModel.isShadeLayoutWide
-
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
// This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
@@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
- Clock(scale = 1f, onClick = viewModel::onClockClicked)
+ Clock(onClick = viewModel::onClockClicked)
VariableDayDate(
longerDateText = viewModel.longerDateText,
shorterDateText = viewModel.shorterDateText,
@@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader(
Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
.padding(horizontal = horizontalPadding),
) {
- if (isShadeLayoutWide) {
+ if (isSplitShade) {
ShadeCarrierGroup(viewModel = viewModel)
}
SystemIconChip(
- onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide }
+ onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade }
) {
StatusIcons(
viewModel = viewModel,
@@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader(
.defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
- Box {
- Clock(
- scale = 2.57f,
- onClick = viewModel::onClockClicked,
- modifier = Modifier.align(Alignment.CenterStart),
- )
- }
+ Clock(
+ onClick = viewModel::onClockClicked,
+ modifier = Modifier.align(Alignment.CenterStart),
+ scale = 2.57f,
+ )
Box(
modifier =
Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth()
@@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader(
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
- val isShadeLayoutWide = viewModel.isShadeLayoutWide
-
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
// This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
@@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader(
startContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
val chipHighlight = viewModel.notificationsChipHighlight
- if (isShadeLayoutWide) {
+ if (viewModel.showClock) {
Clock(
- scale = 1f,
onClick = viewModel::onClockClicked,
modifier = Modifier.padding(horizontal = 4.dp),
)
- Spacer(modifier = Modifier.width(5.dp))
}
NotificationsChip(
onClick = viewModel::onNotificationIconChipClicked,
@@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader(
}
@Composable
-private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) {
+private fun ContentScope.Clock(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ scale: Float = 1f,
+) {
val layoutDirection = LocalLayoutDirection.current
ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 5040490da8f6..885d34fb95c9 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateContentDpAsState
+import com.android.compose.animation.scene.animateContentFloatAsState
import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.padding
@@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene(
viewModel = viewModel,
headerViewModel = headerViewModel,
notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
mediaCarouselController = mediaCarouselController,
mediaHost = qqsMediaHost,
modifier = modifier,
@@ -253,9 +251,6 @@ private fun ContentScope.SingleShade(
viewModel: ShadeSceneContentViewModel,
headerViewModel: ShadeHeaderViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
mediaCarouselController: MediaCarouselController,
mediaHost: MediaHost,
modifier: Modifier = Modifier,
@@ -340,6 +335,7 @@ private fun ContentScope.SingleShade(
content = {
CollapsedShadeHeader(
viewModel = headerViewModel,
+ isSplitShade = false,
modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
)
@@ -434,15 +430,13 @@ private fun ContentScope.SplitShade(
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
val tileSquishiness by
- animateSceneFloatAsState(
+ animateContentFloatAsState(
value = 1f,
key = QuickSettings.SharedValues.TilesSquishiness,
canOverflow = false,
)
val unfoldTranslationXForStartSide by
viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f)
- val unfoldTranslationXForEndSide by
- viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f)
val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings)
val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
@@ -512,6 +506,7 @@ private fun ContentScope.SplitShade(
Column(modifier = Modifier.fillMaxSize()) {
CollapsedShadeHeader(
viewModel = headerViewModel,
+ isSplitShade = true,
modifier =
Modifier.then(brightnessMirrorShowingModifier)
.padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 72ee75ad2d47..90bf92ae1dd0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -32,12 +32,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ApproachLayoutModifierNode
+import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
-import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.zIndex
import com.android.compose.animation.scene.Ancestor
import com.android.compose.animation.scene.AnimatedState
import com.android.compose.animation.scene.ContentKey
@@ -68,8 +73,8 @@ import com.android.compose.gesture.NestedScrollControlState
import com.android.compose.gesture.NestedScrollableBound
import com.android.compose.gesture.nestedScrollController
import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.graphics.ContainerNode
import com.android.compose.ui.graphics.ContainerState
-import com.android.compose.ui.graphics.container
import kotlin.math.pow
/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
@@ -158,24 +163,14 @@ internal sealed class Content(
fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) {
// If this content has a custom factory, provide it to the content so that the factory is
// automatically used when calling rememberOverscrollEffect().
+ val isElevationPossible =
+ layoutImpl.state.isElevationPossible(content = key, element = null)
Box(
- modifier
- .thenIf(isInvisible) { InvisibleModifier }
- .zIndex(zIndex)
- .approachLayout(
- isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
- ) { measurable, constraints ->
- // TODO(b/353679003): Use the ModifierNode API to set this *before* the
- // approach
- // pass is started.
- targetSize = lookaheadSize
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
- Modifier.container(containerState)
- }
- .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
+ modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf(
+ layoutImpl.implicitTestTags
+ ) {
+ Modifier.testTag(key.testTag)
+ }
) {
CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
scope.content()
@@ -194,6 +189,72 @@ internal sealed class Content(
}
}
+private data class ContentElement(
+ private val content: Content,
+ private val isElevationPossible: Boolean,
+ private val isInvisible: Boolean,
+) : ModifierNodeElement<ContentNode>() {
+ override fun create(): ContentNode = ContentNode(content, isElevationPossible, isInvisible)
+
+ override fun update(node: ContentNode) {
+ node.update(content, isElevationPossible, isInvisible)
+ }
+}
+
+private class ContentNode(
+ private var content: Content,
+ private var isElevationPossible: Boolean,
+ private var isInvisible: Boolean,
+) : DelegatingNode(), ApproachLayoutModifierNode {
+ private var containerDelegate = containerDelegate(isElevationPossible)
+
+ private fun containerDelegate(isElevationPossible: Boolean): ContainerNode? {
+ return if (isElevationPossible) delegate(ContainerNode(content.containerState)) else null
+ }
+
+ fun update(content: Content, isElevationPossible: Boolean, isInvisible: Boolean) {
+ if (content != this.content || isElevationPossible != this.isElevationPossible) {
+ this.content = content
+ this.isElevationPossible = isElevationPossible
+
+ containerDelegate?.let { undelegate(it) }
+ containerDelegate = containerDelegate(isElevationPossible)
+ }
+
+ this.isInvisible = isInvisible
+ }
+
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ check(isLookingAhead)
+ return measurable.measure(constraints).run {
+ content.targetSize = IntSize(width, height)
+ layout(width, height) {
+ if (!isInvisible) {
+ place(0, 0, zIndex = content.zIndex)
+ }
+ }
+ }
+ }
+
+ override fun ApproachMeasureScope.approachMeasure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ return measurable.measure(constraints).run {
+ layout(width, height) {
+ if (!isInvisible) {
+ place(0, 0, zIndex = content.zIndex)
+ }
+ }
+ }
+ }
+}
+
internal class ContentEffects(factory: OverscrollFactory) {
val overscrollEffect = factory.createOverscrollEffect()
val gestureEffect = GestureEffect(overscrollEffect)
@@ -307,8 +368,3 @@ internal class ContentScopeImpl(
)
}
}
-
-private val InvisibleModifier =
- Modifier.layout { measurable, constraints ->
- measurable.measure(constraints).run { layout(width, height) {} }
- }
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
index 0fcdfa3e1b53..57f67665242c 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
@@ -60,7 +60,9 @@
912,
928,
944,
- 960
+ 960,
+ 976,
+ 992
],
"features": [
{
@@ -310,6 +312,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -491,67 +501,75 @@
},
{
"width": 170.4,
- "height": 39.2
+ "height": 28.8
},
{
"width": 166.8,
- "height": 36
+ "height": 15.2
},
{
"width": 164,
- "height": 31.6
+ "height": 6.4
},
{
"width": 162.4,
- "height": 26.8
+ "height": 0.8
},
{
"width": 161.2,
- "height": 22
+ "height": 0
},
{
"width": 160.4,
- "height": 17.6
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
- "height": 14
+ "height": 0
},
{
"width": 160,
- "height": 10.8
+ "height": 0
},
{
"width": 160,
- "height": 8
+ "height": 0
},
{
"width": 160,
- "height": 6
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 2.8
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
},
{
"width": 160,
@@ -627,6 +645,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
index 3196334c5314..01bc852cf7f4 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
@@ -59,7 +59,9 @@
896,
912,
928,
- 944
+ 944,
+ 960,
+ 976
],
"features": [
{
@@ -305,6 +307,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -482,71 +492,79 @@
},
{
"width": 171.6,
- "height": 39.6
+ "height": 32
},
{
"width": 167.6,
- "height": 36.8
+ "height": 18
},
{
"width": 164.8,
- "height": 32.4
+ "height": 8.8
},
{
"width": 162.8,
- "height": 27.6
+ "height": 2.8
},
{
"width": 161.6,
- "height": 22.8
+ "height": 0
},
{
"width": 160.8,
- "height": 18.4
+ "height": 0
},
{
"width": 160.4,
- "height": 14.4
+ "height": 0
},
{
"width": 160,
- "height": 11.2
+ "height": 0
},
{
"width": 160,
- "height": 8.4
+ "height": 0
},
{
"width": 160,
- "height": 6.4
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 3.2
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.6
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -617,6 +635,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
index 4b0306853903..b6e423afc6c4 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
@@ -336,55 +336,55 @@
},
{
"width": 188,
- "height": 24.8
+ "height": 25.6
},
{
"width": 188,
- "height": 32.8
+ "height": 36.4
},
{
"width": 188,
- "height": 44
+ "height": 45.6
},
{
"width": 188,
- "height": 57.2
+ "height": 59.2
},
{
"width": 188,
- "height": 70.8
+ "height": 72.8
},
{
"width": 188,
- "height": 78
+ "height": 79.6
},
{
"width": 188,
- "height": 91.2
+ "height": 92.8
},
{
"width": 188,
- "height": 103.2
+ "height": 104.4
},
{
"width": 188,
- "height": 114.4
+ "height": 115.2
},
{
"width": 188,
- "height": 124.4
+ "height": 125.2
},
{
"width": 188,
- "height": 134
+ "height": 134.8
},
{
"width": 188,
- "height": 142.8
+ "height": 143.2
},
{
"width": 188,
- "height": 150.8
+ "height": 151.2
},
{
"width": 188,
@@ -392,7 +392,7 @@
},
{
"width": 188,
- "height": 159.6
+ "height": 160
},
{
"width": 188,
@@ -400,7 +400,7 @@
},
{
"width": 188,
- "height": 174
+ "height": 174.4
},
{
"width": 188,
@@ -412,7 +412,7 @@
},
{
"width": 188,
- "height": 187.6
+ "height": 188
},
{
"width": 188,
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
index 10a9ba7e2760..a82db346ed58 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
@@ -39,7 +39,9 @@
576,
592,
608,
- 624
+ 624,
+ 640,
+ 656
],
"features": [
{
@@ -205,6 +207,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -302,67 +312,75 @@
},
{
"width": 170.8,
- "height": 39.6
+ "height": 29.6
},
{
"width": 166.8,
- "height": 36.8
+ "height": 12.8
},
{
"width": 164,
- "height": 32.4
+ "height": 2.4
},
{
"width": 162,
- "height": 27.6
+ "height": 0
},
{
"width": 160.8,
- "height": 22.8
+ "height": 0
},
{
"width": 160.4,
- "height": 18.4
+ "height": 0
},
{
"width": 160,
- "height": 14.4
+ "height": 0
},
{
"width": 160,
- "height": 11.2
+ "height": 0
},
{
"width": 160,
- "height": 8.4
+ "height": 0
},
{
"width": 160,
- "height": 6
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 3.2
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -417,6 +435,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
index d8bf48d32d20..6dc5a0e79e81 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
@@ -32,8 +32,7 @@
464,
480,
496,
- 512,
- 528
+ 512
],
"features": [
{
@@ -163,10 +162,6 @@
{
"x": 50,
"y": 50
- },
- {
- "x": 50,
- "y": 50
}
]
},
@@ -228,63 +223,59 @@
},
{
"width": 188,
- "height": 42.4
+ "height": 44.4
},
{
"width": 188,
- "height": 100
+ "height": 103.6
},
{
"width": 188,
- "height": 161.6
+ "height": 166
},
{
"width": 188,
- "height": 218
+ "height": 222.4
},
{
"width": 188,
- "height": 265.6
+ "height": 270
},
{
"width": 188,
- "height": 303.6
+ "height": 307.2
},
{
"width": 188,
- "height": 332.4
+ "height": 335.6
},
{
"width": 188,
- "height": 354
+ "height": 356.4
},
{
"width": 188,
- "height": 369.2
+ "height": 371.2
},
{
"width": 188,
- "height": 380
+ "height": 381.6
},
{
"width": 188,
- "height": 387.2
+ "height": 388.8
},
{
"width": 188,
- "height": 392
+ "height": 393.2
},
{
"width": 188,
- "height": 395.2
+ "height": 396
},
{
"width": 188,
- "height": 397.6
- },
- {
- "width": 188,
- "height": 398.4
+ "height": 398
},
{
"width": 188,
@@ -356,7 +347,6 @@
1,
1,
1,
- 1,
1
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
index 57bdf3e1ecab..1cd971aa2898 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
@@ -69,9 +69,7 @@
1056,
1072,
1088,
- 1104,
- 1120,
- 1136
+ 1104
],
"features": [
{
@@ -353,14 +351,6 @@
{
"x": 64,
"y": 50
- },
- {
- "x": 64,
- "y": 50
- },
- {
- "x": 64,
- "y": 50
}
]
},
@@ -466,43 +456,43 @@
},
{
"width": 188,
- "height": 24
+ "height": 24.8
},
{
"width": 188,
- "height": 29.6
+ "height": 30
},
{
"width": 188,
- "height": 37.2
+ "height": 38
},
{
"width": 188,
- "height": 45.6
+ "height": 46
},
{
"width": 188,
- "height": 53.6
+ "height": 54
},
{
"width": 188,
- "height": 60.4
+ "height": 61.2
},
{
"width": 188,
- "height": 66.4
+ "height": 66.8
},
{
"width": 188,
- "height": 71.2
+ "height": 71.6
},
{
"width": 188,
- "height": 75.2
+ "height": 75.6
},
{
"width": 188,
- "height": 77.6
+ "height": 78
},
{
"width": 188,
@@ -510,7 +500,7 @@
},
{
"width": 188,
- "height": 80.4
+ "height": 80.8
},
{
"width": 188,
@@ -522,7 +512,7 @@
},
{
"width": 188,
- "height": 79.2
+ "height": 79.6
},
{
"width": 187.6,
@@ -530,7 +520,7 @@
},
{
"width": 186.8,
- "height": 76
+ "height": 76.4
},
{
"width": 186,
@@ -578,43 +568,39 @@
},
{
"width": 172.4,
- "height": 39.2
+ "height": 37.6
},
{
"width": 170.8,
- "height": 38.4
+ "height": 38
},
{
"width": 169.2,
- "height": 34.8
+ "height": 30.4
},
{
"width": 167.6,
- "height": 30
+ "height": 25.2
},
{
"width": 166,
- "height": 25.2
+ "height": 20.4
},
{
"width": 164,
- "height": 20.4
+ "height": 16
},
{
"width": 162.4,
- "height": 16.4
+ "height": 12.4
},
{
"width": 160.8,
- "height": 12.8
- },
- {
- "width": 160,
- "height": 9.6
+ "height": 9.2
},
{
"width": 160,
- "height": 7.2
+ "height": 6.8
},
{
"width": 160,
@@ -626,7 +612,7 @@
},
{
"width": 160,
- "height": 2.8
+ "height": 2.4
},
{
"width": 160,
@@ -634,10 +620,6 @@
},
{
"width": 160,
- "height": 1.2
- },
- {
- "width": 160,
"height": 0.8
},
{
@@ -646,7 +628,7 @@
},
{
"width": 160,
- "height": 0
+ "height": 0.4
},
{
"width": 160,
@@ -735,8 +717,6 @@
0.03147719,
0.019312752,
0.011740655,
- 0,
- 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
index 9aa91c2d5e17..1030455e873f 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
@@ -27,7 +27,9 @@
384,
400,
416,
- 432
+ 432,
+ 448,
+ 464
],
"features": [
{
@@ -145,6 +147,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -194,67 +204,75 @@
},
{
"width": 168.8,
- "height": 38.4
+ "height": 22.4
},
{
"width": 165.2,
- "height": 34.8
+ "height": 10
},
{
"width": 162.8,
- "height": 30
+ "height": 2.4
},
{
"width": 161.2,
- "height": 25.2
+ "height": 0
},
{
"width": 160.4,
- "height": 20.4
+ "height": 0
},
{
"width": 160,
- "height": 16.4
+ "height": 0
},
{
"width": 160,
- "height": 12.8
+ "height": 0
},
{
"width": 160,
- "height": 9.6
+ "height": 0
},
{
"width": 160,
- "height": 7.2
+ "height": 0
},
{
"width": 160,
- "height": 5.2
+ "height": 0
},
{
"width": 160,
- "height": 3.6
+ "height": 0
},
{
"width": 160,
- "height": 2.8
+ "height": 0
},
{
"width": 160,
- "height": 1.6
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -297,6 +315,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java
index 7fb879c02778..f0c9141a76e6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ExpandHelperTest.java
@@ -32,7 +32,7 @@ import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.animation.AnimatorTestRule;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
-import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 0b13900d826b..7e4704a6179a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.shared.QSSettingsPackageRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
@@ -108,6 +109,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
private static final String TEST_LABEL = "label";
private static final int TEST_PRESET_INDEX = 1;
private static final String TEST_PRESET_NAME = "test_preset";
+ private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
@@ -137,6 +139,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
@Mock
private HearingDevicesUiEventLogger mUiEventLogger;
@Mock
+ private QSSettingsPackageRepository mQSSettingsPackageRepository;
+ @Mock
private CachedBluetoothDevice mCachedDevice;
@Mock
private BluetoothDevice mDevice;
@@ -164,6 +168,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(List.of(mCachedDevice));
when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
+ when(mQSSettingsPackageRepository.getSettingsPackageName())
+ .thenReturn(SETTINGS_PACKAGE_NAME);
when(mDevice.getBondState()).thenReturn(BOND_BONDED);
when(mDevice.isConnected()).thenReturn(true);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -195,6 +201,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
anyInt(), any());
assertThat(intentCaptor.getValue().getAction()).isEqualTo(
Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME);
verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR,
TEST_LAUNCH_SOURCE_ID);
}
@@ -210,6 +217,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
anyInt(), any());
assertThat(intentCaptor.getValue().getAction()).isEqualTo(
HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS);
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME);
verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK,
TEST_LAUNCH_SOURCE_ID);
}
@@ -392,7 +400,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
mExecutor,
mExecutor,
mAudioManager,
- mUiEventLogger
+ mUiEventLogger,
+ mQSSettingsPackageRepository
);
mDialog = mDialogDelegate.createDialog();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
index 684af6fc8040..efc68f3b6884 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
@@ -64,10 +64,10 @@ class KeyEventInteractorTest : SysuiTestCase() {
val isPowerButtonLongPressed by collectLastValue(
underTest.isPowerButtonLongPressed)
- repository.setPowerButtonBeingLongPressed(false)
+ repository.setPowerButtonLongPressed(false)
assertThat(isPowerButtonLongPressed).isFalse()
- repository.setPowerButtonBeingLongPressed(true)
+ repository.setPowerButtonLongPressed(true)
assertThat(isPowerButtonLongPressed).isTrue()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
index 909acca12551..573216dd680a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -49,9 +49,9 @@ import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SystemUIInitializerImpl;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
@@ -177,7 +177,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase {
@Test
public void schedulesAlarm12hBefore() {
long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(16);
- AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null);
+ AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours,
+ null);
mProvider.onNextAlarmChanged(alarmClockInfo);
long twelveHours = TimeUnit.HOURS.toMillis(KeyguardSliceProvider.ALARM_VISIBILITY_HOURS);
@@ -189,7 +190,8 @@ public class KeyguardSliceProviderTest extends SysuiTestCase {
@Test
public void updatingNextAlarmInvalidatesSlice() {
long in16Hours = System.currentTimeMillis() + TimeUnit.HOURS.toHours(8);
- AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours, null);
+ AlarmManager.AlarmClockInfo alarmClockInfo = new AlarmManager.AlarmClockInfo(in16Hours,
+ null);
mProvider.onNextAlarmChanged(alarmClockInfo);
verify(mContentResolver).notifyChange(eq(mProvider.getUri()), eq(null));
@@ -204,7 +206,7 @@ public class KeyguardSliceProviderTest extends SysuiTestCase {
@Test
public void addZenMode_addedToSlice() {
ListBuilder listBuilder = spy(new ListBuilder(getContext(), mProvider.getUri(),
- ListBuilder.INFINITY));
+ ListBuilder.INFINITY));
mProvider.addZenModeLocked(listBuilder);
verify(listBuilder, never()).addRow(any(ListBuilder.RowBuilder.class));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt
index 2558d583b001..89a53f5722ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractorTest.kt
@@ -49,6 +49,7 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
class KeyguardStateCallbackInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
@@ -81,7 +82,6 @@ class KeyguardStateCallbackInteractorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun test_lockscreenVisibility_notifyDismissSucceeded_ifNotVisible() =
testScope.runTest {
underTest.addCallback(callback)
@@ -109,7 +109,6 @@ class KeyguardStateCallbackInteractorTest : SysuiTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun test_lockscreenVisibility_reportsKeyguardShowingChanged() =
testScope.runTest {
underTest.addCallback(callback)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt
index 8533134fd94e..95a6e56717fa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModelTest.kt
@@ -21,15 +21,20 @@ import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_NOTIFICATION_SHADE_BLUR
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.BrokenWithSceneContainer
+import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.transitions.blurConfig
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -43,6 +48,7 @@ class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterizati
SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val repository = kosmos.fakeKeyguardTransitionRepository
private lateinit var underTest: OccludedToPrimaryBouncerTransitionViewModel
companion object {
@@ -63,6 +69,25 @@ class OccludedToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterizati
}
@Test
+ @DisableSceneContainer
+ fun lockscreenAlphaImmediatelyToZero() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.lockscreenAlpha)
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
@BrokenWithSceneContainer(388068805)
fun notificationsAreBlurredImmediatelyWhenBouncerIsOpenedAndShadeIsExpanded() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt
index a7fe586cbfa5..10b00857e887 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/NotificationMediaManagerTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar
+package com.android.systemui.media
import android.media.MediaMetadata
import android.media.session.MediaController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index 2db2199602b8..9c4d93c17d00 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -48,10 +48,12 @@ import androidx.test.filters.SmallTest;
import com.android.media.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.res.R;
import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListeningExecutorService;
import org.junit.Before;
import org.junit.Test;
@@ -61,6 +63,7 @@ import org.mockito.Captor;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@SmallTest
@@ -95,6 +98,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
private List<MediaDevice> mMediaDevices = new ArrayList<>();
private List<MediaItem> mMediaItems = new ArrayList<>();
MediaOutputSeekbar mSpyMediaOutputSeekbar;
+ Executor mMainExecutor = mContext.getMainExecutor();
+ ListeningExecutorService mBackgroundExecutor = ThreadUtils.getBackgroundExecutor();
@Before
public void setUp() {
@@ -108,6 +113,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
when(mMediaSwitchingController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
when(mMediaSwitchingController.getSessionVolume()).thenReturn(TEST_CURRENT_VOLUME);
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
+ when(mMediaSwitchingController.getColorSchemeLegacy()).thenReturn(
+ mock(MediaOutputColorSchemeLegacy.class));
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
@@ -122,7 +129,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true));
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false));
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -148,7 +156,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
@Test
public void onBindViewHolder_bindPairNew_verifyView() {
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -173,7 +182,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -195,7 +205,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(null);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -665,7 +676,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
@Test
public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -683,7 +695,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -701,7 +714,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController,
+ mContext.getMainExecutor(), ThreadUtils.getBackgroundExecutor());
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -723,7 +737,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -778,6 +793,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -799,6 +816,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -820,6 +839,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -841,6 +862,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -862,6 +885,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_ACTIVE_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
@@ -883,6 +908,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mTitleText.getAlpha())
+ .isEqualTo(MediaOutputAdapterLegacy.DEVICE_DISABLED_ALPHA);
assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
mViewHolder.mContainerLayout.performClick();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
index 00ee1c36590c..1b497a2b36ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
@@ -24,6 +24,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager
import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
@@ -40,6 +41,7 @@ import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
@EnabledOnRavenwood
@@ -53,14 +55,18 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() {
@Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var dialogManager: HearingDevicesDialogManager
+ @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository
@Before
fun setUp() {
+ whenever(settingsPackageRepository.getSettingsPackageName())
+ .thenReturn(SETTINGS_PACKAGE_NAME)
underTest =
HearingDevicesTileUserActionInteractor(
testScope.coroutineContext,
inputHandler,
dialogManager,
+ settingsPackageRepository,
)
}
@@ -91,6 +97,11 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() {
QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
assertThat(it.intent.action).isEqualTo(Settings.ACTION_HEARING_DEVICES_SETTINGS)
+ assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE_NAME)
}
}
+
+ companion object {
+ private const val SETTINGS_PACKAGE_NAME = "com.android.settings"
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
index e43c46b36a06..dd0ba00994ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/display/StatusBarTouchShadeDisplayPolicyTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.display
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.MotionEvent
@@ -31,6 +32,7 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.shade.data.repository.statusBarTouchShadeDisplayPolicy
import com.android.systemui.shade.domain.interactor.notificationElement
import com.android.systemui.shade.domain.interactor.qsElement
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
@@ -41,6 +43,7 @@ import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(ShadeWindowGoesAround.FLAG_NAME)
class StatusBarTouchShadeDisplayPolicyTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
index 03f546b09faf..24593f4455e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractorTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.shade.domain.interactor
import android.content.res.Configuration
import android.content.res.mockResources
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,6 +28,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.scene.ui.view.mockShadeRootView
import com.android.systemui.shade.data.repository.fakeShadeDisplaysRepository
+import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
import com.android.systemui.statusbar.notification.row.notificationRebindingTracker
@@ -47,6 +49,7 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
+@EnableFlags(ShadeWindowGoesAround.FLAG_NAME)
class ShadeDisplaysInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index a832f486ef32..04eb709b8894 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -94,6 +94,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
}
@Test
+ fun showClock_wideLayout_returnsTrue() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = true)
+
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
+ assertThat(underTest.showClock).isTrue()
+
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
+ assertThat(underTest.showClock).isTrue()
+ }
+
+ @Test
+ fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
+
+ assertThat(underTest.showClock).isFalse()
+ }
+
+ @Test
+ fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
+
+ assertThat(underTest.showClock).isTrue()
+ }
+
+ @Test
fun onShadeCarrierGroupClicked_launchesNetworkSettings() =
testScope.runTest {
val activityStarter = kosmos.activityStarter
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 064fd485dab4..b80ff3466007 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.log.LogAssertKt;
import com.android.systemui.plugins.Plugin;
import com.android.systemui.plugins.PluginLifecycleManager;
import com.android.systemui.plugins.PluginListener;
@@ -138,11 +139,12 @@ public class PluginInstanceTest extends SysuiTestCase {
mVersionCheckResult = false;
assertFalse(mPluginInstance.hasError());
-
mPluginInstanceFactory.create(
mContext, mAppInfo, wrongVersionTestPluginComponentName,
TestPlugin.class, mPluginListener);
- mPluginInstance.onCreate();
+ LogAssertKt.assertRunnableLogsWtf(()-> {
+ mPluginInstance.onCreate();
+ });
assertTrue(mPluginInstance.hasError());
assertNull(mPluginInstance.getPlugin());
}
@@ -193,8 +195,10 @@ public class PluginInstanceTest extends SysuiTestCase {
mPluginInstance.onCreate();
assertFalse(mPluginInstance.hasError());
- Object result = mPluginInstance.getPlugin().methodThrowsError();
- assertNotNull(result); // Wrapper function should return non-null;
+ LogAssertKt.assertRunnableLogsWtf(()-> {
+ Object result = mPluginInstance.getPlugin().methodThrowsError();
+ assertNotNull(result); // Wrapper function should return non-null;
+ });
assertTrue(mPluginInstance.hasError());
assertNull(mPluginInstance.getPlugin());
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 5d1950670777..7f8f5f43e775 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
@@ -39,6 +40,7 @@ import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(StatusBarNotifChips.FLAG_NAME)
class SingleNotificationChipInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
val factory = kosmos.singleNotificationChipInteractorFactory
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index e22acd53e584..9804932918dc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -37,6 +37,7 @@ import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.log.assertLogsWtfs
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shadeTestUtil
@@ -205,8 +206,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
fun pinnedHeadsUpStatuses_pinnedByUser_butFlagOff_returnsNotPinned() {
val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
entry.row = testHelper.createRow()
- underTest.showNotification(entry, isPinnedByUser = true)
-
+ assertLogsWtfs { underTest.showNotification(entry, isPinnedByUser = true) }
assertThat(underTest.hasPinnedHeadsUp()).isFalse()
assertThat(underTest.pinnedHeadsUpStatus()).isEqualTo(PinnedStatus.NotPinned)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 8e7733bb23ca..6415f8c25a37 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -60,11 +60,11 @@ import com.android.systemui.TestableDependency;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.media.dialog.MediaOutputDialogManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.SmartReplyController;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
index f6c031f54818..8e3bdc48398f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt
@@ -219,7 +219,6 @@ class StackStateAnimatorTest : SysuiTestCase() {
)
}
- @DisableFlags(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT)
@Test
@EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
fun startAnimationForEvents_headsUpFromBottom_startsHeadsUpAppearAnim_flagOn() {
@@ -246,7 +245,7 @@ class StackStateAnimatorTest : SysuiTestCase() {
}
@Test
- @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
+ @DisableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME, Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT)
fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOff() {
val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()
val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
@@ -277,6 +276,7 @@ class StackStateAnimatorTest : SysuiTestCase() {
@Test
@EnableFlags(NotificationsHunSharedAnimationValues.FLAG_NAME)
+ @DisableFlags(Flags.FLAG_PHYSICAL_NOTIFICATION_MOVEMENT)
fun startAnimationForEvents_startsHeadsUpDisappearAnim_flagOn() {
val disappearDuration = ANIMATION_DURATION_HEADS_UP_DISAPPEAR.toLong()
val event = AnimationEvent(view, AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index 14e7cdc50227..3b836b774788 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -16,11 +16,11 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
-import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
@@ -32,7 +32,7 @@ import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
-@RunWithLooper
+@EnableSceneContainer
class NotificationsPlaceholderViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val underTest by lazy { kosmos.notificationsPlaceholderViewModel }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index e2330f448a1b..1ea41de63e64 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -61,8 +61,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.log.SessionTracker;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt
index c89dc5722c7a..33689931e6ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,789 +26,701 @@ import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS
+import com.android.systemui.flags.fake
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileMappingsProxy
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileIconInteractorKairosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ featureFlagsClassic.fake.apply { setDefault(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS) }
+ }
- private lateinit var underTest: MobileIconInteractorKairos
- private val mobileMappingsProxy = FakeMobileMappingsProxy()
- private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock())
+ private val Kosmos.tableLogBuffer by Fixture {
+ logcatTableLogBuffer(this, "MobileIconInteractorKairosTest")
+ }
- private val connectionRepository =
- FakeMobileConnectionRepository(
- SUB_1_ID,
- logcatTableLogBuffer(kosmos, "MobileIconInteractorTest"),
- )
+ private var Kosmos.overrides: MobileIconCarrierIdOverrides by Fixture {
+ MobileIconCarrierIdOverridesImpl()
+ }
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val Kosmos.defaultSubscriptionHasDataEnabled by Fixture { MutableState(kairos, true) }
- @Before
- fun setUp() {
- underTest = createInteractor()
+ private val Kosmos.alwaysShowDataRatIcon by Fixture { MutableState(kairos, false) }
- mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
- connectionRepository.isInService.value = true
- }
+ private val Kosmos.alwaysUseCdmaLevel by Fixture { MutableState(kairos, false) }
- @Test
- fun gsm_usesGsmLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = true
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
+ private val Kosmos.isSingleCarrier by Fixture { MutableState(kairos, true) }
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ private val Kosmos.mobileIsDefault by Fixture { MutableState(kairos, false) }
- assertThat(latest).isEqualTo(GSM_LEVEL)
+ private val Kosmos.defaultMobileIconMapping by Fixture {
+ MutableState(kairos, fakeMobileIconsInteractor.TEST_MAPPING)
+ }
- job.cancel()
- }
+ private val Kosmos.defaultMobileIconGroup by Fixture { MutableState(kairos, TelephonyIcons.G) }
- @Test
- fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = true
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
- mobileIconsInteractor.alwaysUseCdmaLevel.value = true
+ private val Kosmos.isDefaultConnectionFailed by Fixture { MutableState(kairos, false) }
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ private val Kosmos.isForceHidden by Fixture { MutableState(kairos, false) }
- assertThat(latest).isEqualTo(GSM_LEVEL)
+ private val Kosmos.underTest by ActivatedKairosFixture {
+ MobileIconInteractorKairosImpl(
+ defaultSubscriptionHasDataEnabled,
+ alwaysShowDataRatIcon,
+ alwaysUseCdmaLevel,
+ isSingleCarrier,
+ mobileIsDefault,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ isDefaultConnectionFailed,
+ isForceHidden,
+ connectionRepository = connectionRepo,
+ context = context,
+ carrierIdOverrides = overrides,
+ )
+ }
- job.cancel()
+ private val Kosmos.connectionRepo by Fixture {
+ FakeMobileConnectionRepositoryKairos(SUB_1_ID, kairos, tableLogBuffer).apply {
+ dataEnabled.setValue(true)
+ isInService.setValue(true)
}
+ }
+
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
@Test
- fun notGsm_level_default_unknown() =
- testScope.runTest {
- connectionRepository.isGsm.value = false
+ fun gsm_usesGsmLevel() = runTest {
+ connectionRepo.isGsm.setValue(true)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(GSM_LEVEL)
+ }
@Test
- fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = false
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
- mobileIconsInteractor.alwaysUseCdmaLevel.value = true
+ fun gsm_alwaysShowCdmaTrue_stillUsesGsmLevel() = runTest {
+ connectionRepo.isGsm.setValue(true)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
+ // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(true)
+ alwaysUseCdmaLevel.setValue(true)
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- assertThat(latest).isEqualTo(CDMA_LEVEL)
-
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(GSM_LEVEL)
+ }
@Test
- fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() =
- testScope.runTest {
- connectionRepository.isGsm.value = false
- connectionRepository.primaryLevel.value = GSM_LEVEL
- connectionRepository.cdmaLevel.value = CDMA_LEVEL
- mobileIconsInteractor.alwaysUseCdmaLevel.value = false
-
- var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.level }.launchIn(this)
+ fun notGsm_level_default_unknown() = runTest {
+ connectionRepo.isGsm.setValue(false)
- assertThat(latest).isEqualTo(GSM_LEVEL)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+ }
@Test
- fun numberOfLevels_comesFromRepo_whenApplicable() =
- testScope.runTest {
- var latest: Int? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = (it as? SignalIconModel.Cellular)?.numberOfLevels }
- .launchIn(this)
-
- connectionRepository.numberOfLevels.value = 5
- assertThat(latest).isEqualTo(5)
+ fun notGsm_alwaysShowCdmaTrue_usesCdmaLevel() = runTest {
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
+ // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(true)
+ alwaysUseCdmaLevel.setValue(true)
- connectionRepository.numberOfLevels.value = 4
- assertThat(latest).isEqualTo(4)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat(latest?.level).isEqualTo(CDMA_LEVEL)
+ }
@Test
- fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() =
- testScope.runTest {
- connectionRepository.inflateSignalStrength.value = false
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun notGsm_alwaysShowCdmaFalse_usesPrimaryLevel() = runTest {
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.primaryLevel.setValue(GSM_LEVEL)
+ connectionRepo.cdmaLevel.setValue(CDMA_LEVEL)
+ // mobileIconsInteractor.alwaysUseCdmaLevel.setValue(false)
+ alwaysUseCdmaLevel.setValue(false)
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
-
- // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level
- assertThat(latest!!.level).isEqualTo(5)
- }
+ assertThat(latest?.level).isEqualTo(GSM_LEVEL)
+ }
@Test
- fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = true
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun numberOfLevels_comesFromRepo_whenApplicable() = runTest {
+ val latest by
+ underTest.signalLevelIcon
+ .map { (it as? SignalIconModel.Cellular)?.numberOfLevels }
+ .collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = true
+ connectionRepo.numberOfLevels.setValue(5)
+ assertThat(latest).isEqualTo(5)
- assertThat(latest).isTrue()
- }
+ connectionRepo.numberOfLevels.setValue(4)
+ assertThat(latest).isEqualTo(4)
+ }
@Test
- fun networkSlice_configOn_noPrioritizedCaps_noSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = true
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun inflateSignalStrength_arbitrarilyAddsOneToTheReportedLevel() = runTest {
+ connectionRepo.inflateSignalStrength.setValue(false)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = false
+ connectionRepo.primaryLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- assertThat(latest).isFalse()
- }
+ connectionRepo.inflateSignalStrength.setValue(true)
+ connectionRepo.primaryLevel.setValue(4)
+
+ // when INFLATE_SIGNAL_STRENGTH is true, we add 1 to the reported signal level
+ assertThat(latest!!.level).isEqualTo(5)
+ }
@Test
- fun networkSlice_configOff_hasPrioritizedCaps_noSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = false
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun networkSlice_configOn_hasPrioritizedCaps_showsSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(true)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = true
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(true)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun networkSlice_configOff_noPrioritizedCaps_noSlice() =
- testScope.runTest {
- connectionRepository.allowNetworkSliceIndicator.value = false
- val latest by collectLastValue(underTest.showSliceAttribution)
+ fun networkSlice_configOn_noPrioritizedCaps_noSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(true)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- connectionRepository.hasPrioritizedNetworkCapabilities.value = false
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun iconGroup_three_g() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
-
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ fun networkSlice_configOff_hasPrioritizedCaps_noSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(false)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G))
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(true)
- job.cancel()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun iconGroup_updates_on_change() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ fun networkSlice_configOff_noPrioritizedCaps_noSlice() = runTest {
+ connectionRepo.allowNetworkSliceIndicator.setValue(false)
+ val latest by underTest.showSliceAttribution.collectLastValue()
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ connectionRepo.hasPrioritizedNetworkCapabilities.setValue(false)
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G))
-
- assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.FOUR_G))
-
- job.cancel()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun iconGroup_5g_override_type() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE))
-
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ fun iconGroup_three_g() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ )
- assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.NR_5G))
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G))
+ }
@Test
- fun iconGroup_default_if_no_lookup() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN))
+ fun iconGroup_updates_on_change() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ )
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- assertThat(latest)
- .isEqualTo(NetworkTypeIconModel.DefaultIcon(FakeMobileIconsInteractor.DEFAULT_ICON))
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(FOUR_G))
+ )
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.FOUR_G))
+ }
@Test
- fun iconGroup_carrierMerged_usesOverride() =
- testScope.runTest {
- connectionRepository.resolvedNetworkType.value = CarrierMergedNetworkType
-
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ fun iconGroup_5g_override_type() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE))
+ )
- assertThat(latest)
- .isEqualTo(
- NetworkTypeIconModel.DefaultIcon(CarrierMergedNetworkType.iconGroupOverride)
- )
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkTypeIconModel.DefaultIcon(TelephonyIcons.NR_5G))
+ }
@Test
- fun overrideIcon_usesCarrierIdOverride() =
- testScope.runTest {
- val overrides =
- mock<MobileIconCarrierIdOverrides>().also {
- whenever(it.carrierIdEntryExists(anyInt())).thenReturn(true)
- whenever(it.getOverrideFor(anyInt(), anyString(), any())).thenReturn(1234)
- }
+ fun iconGroup_default_if_no_lookup() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN))
+ )
- underTest = createInteractor(overrides)
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- connectionRepository.resolvedNetworkType.value =
- DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ assertThat(latest)
+ .isEqualTo(NetworkTypeIconModel.DefaultIcon(FakeMobileIconsInteractor.DEFAULT_ICON))
+ }
- var latest: NetworkTypeIconModel? = null
- val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+ @Test
+ fun iconGroup_carrierMerged_usesOverride() = runTest {
+ connectionRepo.resolvedNetworkType.setValue(CarrierMergedNetworkType)
- assertThat(latest)
- .isEqualTo(NetworkTypeIconModel.OverriddenIcon(TelephonyIcons.THREE_G, 1234))
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest)
+ .isEqualTo(NetworkTypeIconModel.DefaultIcon(CarrierMergedNetworkType.iconGroupOverride))
+ }
@Test
- fun alwaysShowDataRatIcon_matchesParent() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+ fun overrideIcon_usesCarrierIdOverride() = runTest {
+ overrides =
+ mock<MobileIconCarrierIdOverrides> {
+ on { carrierIdEntryExists(anyInt()) } doReturn true
+ on { getOverrideFor(anyInt(), anyString(), any()) } doReturn 1234
+ }
- mobileIconsInteractor.alwaysShowDataRatIcon.value = true
- assertThat(latest).isTrue()
+ connectionRepo.resolvedNetworkType.setValue(
+ DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
+ )
- mobileIconsInteractor.alwaysShowDataRatIcon.value = false
- assertThat(latest).isFalse()
+ val latest by underTest.networkTypeIconGroup.collectLastValue()
- job.cancel()
- }
+ assertThat(latest)
+ .isEqualTo(NetworkTypeIconModel.OverriddenIcon(TelephonyIcons.THREE_G, 1234))
+ }
@Test
- fun dataState_connected() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+ fun alwaysShowDataRatIcon_matchesParent() = runTest {
+ val latest by underTest.alwaysShowDataRatIcon.collectLastValue()
- connectionRepository.dataConnectionState.value = DataConnectionState.Connected
+ // mobileIconsInteractor.alwaysShowDataRatIcon.setValue(true)
+ alwaysShowDataRatIcon.setValue(true)
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
- job.cancel()
- }
+ // mobileIconsInteractor.alwaysShowDataRatIcon.setValue(false)
+ alwaysShowDataRatIcon.setValue(false)
- @Test
- fun dataState_notConnected() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this)
+ assertThat(latest).isFalse()
+ }
- connectionRepository.dataConnectionState.value = DataConnectionState.Disconnected
+ @Test
+ fun dataState_connected() = runTest {
+ val latest by underTest.isDataConnected.collectLastValue()
- assertThat(latest).isFalse()
+ connectionRepo.dataConnectionState.setValue(DataConnectionState.Connected)
- job.cancel()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun isInService_usesRepositoryValue() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isInService.onEach { latest = it }.launchIn(this)
+ fun dataState_notConnected() = runTest {
+ val latest by underTest.isDataConnected.collectLastValue()
- connectionRepository.isInService.value = true
+ connectionRepo.dataConnectionState.setValue(DataConnectionState.Disconnected)
- assertThat(latest).isTrue()
+ assertThat(latest).isFalse()
+ }
- connectionRepository.isInService.value = false
+ @Test
+ fun isInService_usesRepositoryValue() = runTest {
+ val latest by underTest.isInService.collectLastValue()
- assertThat(latest).isFalse()
+ connectionRepo.isInService.setValue(true)
- job.cancel()
- }
+ assertThat(latest).isTrue()
- @Test
- fun roaming_isGsm_usesConnectionModel() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ connectionRepo.isInService.setValue(false)
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = true
- connectionRepository.isRoaming.value = false
+ assertThat(latest).isFalse()
+ }
- assertThat(latest).isFalse()
+ @Test
+ fun roaming_isGsm_usesConnectionModel() = runTest {
+ val latest by underTest.isRoaming.collectLastValue()
- connectionRepository.isRoaming.value = true
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(true)
+ connectionRepo.isRoaming.setValue(false)
- assertThat(latest).isTrue()
+ assertThat(latest).isFalse()
- job.cancel()
- }
+ connectionRepo.isRoaming.setValue(true)
- @Test
- fun roaming_isCdma_usesCdmaRoamingBit() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ assertThat(latest).isTrue()
+ }
- connectionRepository.cdmaRoaming.value = false
- connectionRepository.isGsm.value = false
- connectionRepository.isRoaming.value = true
+ @Test
+ fun roaming_isCdma_usesCdmaRoamingBit() = runTest {
+ val latest by underTest.isRoaming.collectLastValue()
- assertThat(latest).isFalse()
+ connectionRepo.cdmaRoaming.setValue(false)
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.isRoaming.setValue(true)
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = false
- connectionRepository.isRoaming.value = false
+ assertThat(latest).isFalse()
- assertThat(latest).isTrue()
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.isRoaming.setValue(false)
- job.cancel()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun roaming_falseWhileCarrierNetworkChangeActive() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
+ fun roaming_falseWhileCarrierNetworkChangeActive() = runTest {
+ val latest by underTest.isRoaming.collectLastValue()
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = false
- connectionRepository.isRoaming.value = true
- connectionRepository.carrierNetworkChangeActive.value = true
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(false)
+ connectionRepo.isRoaming.setValue(true)
+ connectionRepo.carrierNetworkChangeActive.setValue(true)
- assertThat(latest).isFalse()
+ assertThat(latest).isFalse()
- connectionRepository.cdmaRoaming.value = true
- connectionRepository.isGsm.value = true
+ connectionRepo.cdmaRoaming.setValue(true)
+ connectionRepo.isGsm.setValue(true)
- assertThat(latest).isFalse()
-
- job.cancel()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val testOperatorName = "operatorAlphaShort"
+ fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- // Default network name, operator name is non-null, uses the operator name
- connectionRepository.networkName.value = DEFAULT_NAME_MODEL
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ val testOperatorName = "operatorAlphaShort"
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName))
+ // Default network name, operator name is non-null, uses the operator name
+ connectionRepo.networkName.setValue(DEFAULT_NAME_MODEL)
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- // Default network name, operator name is null, uses the default
- connectionRepository.operatorAlphaShort.value = null
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName))
- assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+ // Default network name, operator name is null, uses the default
+ connectionRepo.operatorAlphaShort.setValue(null)
- // Derived network name, operator name non-null, uses the derived name
- connectionRepository.networkName.value = DERIVED_NAME_MODEL
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
- assertThat(latest).isEqualTo(DERIVED_NAME_MODEL)
+ // Derived network name, operator name non-null, uses the derived name
+ connectionRepo.networkName.setValue(DERIVED_NAME_MODEL)
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- job.cancel()
- }
+ assertThat(latest).isEqualTo(DERIVED_NAME_MODEL)
+ }
@Test
- fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() =
- testScope.runTest {
- var latest: String? = null
- val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
-
- val testOperatorName = "operatorAlphaShort"
+ fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() = runTest {
+ val latest by underTest.carrierName.collectLastValue()
- // Default network name, operator name is non-null, uses the operator name
- connectionRepository.carrierName.value = DEFAULT_NAME_MODEL
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ val testOperatorName = "operatorAlphaShort"
- assertThat(latest).isEqualTo(testOperatorName)
+ // Default network name, operator name is non-null, uses the operator name
+ connectionRepo.carrierName.setValue(DEFAULT_NAME_MODEL)
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- // Default network name, operator name is null, uses the default
- connectionRepository.operatorAlphaShort.value = null
+ assertThat(latest).isEqualTo(testOperatorName)
- assertThat(latest).isEqualTo(DEFAULT_NAME)
+ // Default network name, operator name is null, uses the default
+ connectionRepo.operatorAlphaShort.setValue(null)
- // Derived network name, operator name non-null, uses the derived name
- connectionRepository.carrierName.value =
- NetworkNameModel.SubscriptionDerived(DERIVED_NAME)
- connectionRepository.operatorAlphaShort.value = testOperatorName
+ assertThat(latest).isEqualTo(DEFAULT_NAME)
- assertThat(latest).isEqualTo(DERIVED_NAME)
+ // Derived network name, operator name non-null, uses the derived name
+ connectionRepo.carrierName.setValue(NetworkNameModel.SubscriptionDerived(DERIVED_NAME))
+ connectionRepo.operatorAlphaShort.setValue(testOperatorName)
- job.cancel()
- }
+ assertThat(latest).isEqualTo(DERIVED_NAME)
+ }
@Test
- fun isSingleCarrier_matchesParent() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ fun isSingleCarrier_matchesParent() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- mobileIconsInteractor.isSingleCarrier.value = true
- assertThat(latest).isTrue()
+ // mobileIconsInteractor.isSingleCarrier.setValue(true)
+ isSingleCarrier.setValue(true)
+ assertThat(latest).isTrue()
- mobileIconsInteractor.isSingleCarrier.value = false
- assertThat(latest).isFalse()
-
- job.cancel()
- }
+ // mobileIconsInteractor.isSingleCarrier.setValue(false)
+ isSingleCarrier.setValue(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun isForceHidden_matchesParent() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
-
- mobileIconsInteractor.isForceHidden.value = true
- assertThat(latest).isTrue()
+ fun isForceHidden_matchesParent() = runTest {
+ val latest by underTest.isForceHidden.collectLastValue()
- mobileIconsInteractor.isForceHidden.value = false
- assertThat(latest).isFalse()
+ // mobileIconsInteractor.isForceHidden.setValue(true)
+ isForceHidden.setValue(true)
+ assertThat(latest).isTrue()
- job.cancel()
- }
+ // mobileIconsInteractor.isForceHidden.setValue(false)
+ isForceHidden.setValue(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun isAllowedDuringAirplaneMode_matchesRepo() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
+ fun isAllowedDuringAirplaneMode_matchesRepo() = runTest {
+ val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue()
- connectionRepository.isAllowedDuringAirplaneMode.value = true
- assertThat(latest).isTrue()
+ connectionRepo.isAllowedDuringAirplaneMode.setValue(true)
+ assertThat(latest).isTrue()
- connectionRepository.isAllowedDuringAirplaneMode.value = false
- assertThat(latest).isFalse()
- }
+ connectionRepo.isAllowedDuringAirplaneMode.setValue(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun cellBasedIconId_correctLevel_notCutout() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
- connectionRepository.primaryLevel.value = 1
- connectionRepository.setDataEnabled(false)
- connectionRepository.isNonTerrestrial.value = false
+ fun cellBasedIconId_correctLevel_notCutout() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
+ connectionRepo.primaryLevel.setValue(1)
+ connectionRepo.dataEnabled.setValue(true)
+ connectionRepo.isNonTerrestrial.setValue(false)
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ val latest by
+ underTest.signalLevelIcon.map { it as? SignalIconModel.Cellular }.collectLastValue()
- assertThat(latest?.level).isEqualTo(1)
- assertThat(latest?.showExclamationMark).isFalse()
+ assertThat(latest?.level).isEqualTo(1)
- job.cancel()
- }
+ // TODO: need to provision MobileIconsInteractorKairos#isDefaultConnectionFailed +
+ // defaultSubscriptionHasDataEnabled?
+ assertThat(latest?.showExclamationMark).isEqualTo(false)
+ }
@Test
- fun icon_usesLevelFromInteractor() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
+ fun icon_usesLevelFromInteractor() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.primaryLevel.value = 3
- assertThat(latest!!.level).isEqualTo(3)
+ connectionRepo.primaryLevel.setValue(3)
+ assertThat(latest!!.level).isEqualTo(3)
- connectionRepository.primaryLevel.value = 1
- assertThat(latest!!.level).isEqualTo(1)
-
- job.cancel()
- }
+ connectionRepo.primaryLevel.setValue(1)
+ assertThat(latest!!.level).isEqualTo(1)
+ }
@Test
- fun cellBasedIcon_usesNumberOfLevelsFromInteractor() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
-
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_usesNumberOfLevelsFromInteractor() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
- connectionRepository.numberOfLevels.value = 5
- assertThat(latest!!.numberOfLevels).isEqualTo(5)
+ val latest by
+ underTest.signalLevelIcon.map { it as? SignalIconModel.Cellular }.collectLastValue()
- connectionRepository.numberOfLevels.value = 2
- assertThat(latest!!.numberOfLevels).isEqualTo(2)
+ connectionRepo.numberOfLevels.setValue(5)
+ assertThat(latest!!.numberOfLevels).isEqualTo(5)
- job.cancel()
- }
+ connectionRepo.numberOfLevels.setValue(2)
+ assertThat(latest!!.numberOfLevels).isEqualTo(2)
+ }
@Test
- fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false
-
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.dataEnabled.setValue(false)
+ defaultSubscriptionHasDataEnabled.setValue(false)
- assertThat(latest!!.showExclamationMark).isTrue()
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isTrue()
+ }
@Test
- fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- mobileIconsInteractor.isDefaultConnectionFailed.value = true
+ fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ // mobileIconsInteractor.isDefaultConnectionFailed.setValue(true)
+ isDefaultConnectionFailed.setValue(true)
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ val latest by underTest.signalLevelIcon.collectLastValue()
- assertThat(latest!!.showExclamationMark).isTrue()
-
- job.cancel()
- }
+ assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isTrue()
+ }
@Test
- fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() =
- testScope.runTest {
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
- mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
- mobileIconsInteractor.isDefaultConnectionFailed.value = false
-
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() = runTest {
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
+ connectionRepo.dataEnabled.setValue(true)
+ // mobileIconsInteractor.isDefaultConnectionFailed.setValue(false)
+ isDefaultConnectionFailed.setValue(false)
- assertThat(latest!!.showExclamationMark).isFalse()
+ val latest by underTest.signalLevelIcon.collectLastValue()
- job.cancel()
- }
+ assertThat((latest!! as SignalIconModel.Cellular).showExclamationMark).isFalse()
+ }
@Test
- fun cellBasedIcon_usesEmptyState_whenNotInService() =
- testScope.runTest {
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular }
- .launchIn(this)
+ fun cellBasedIcon_usesEmptyState_whenNotInService() = runTest {
+ val latest by
+ underTest.signalLevelIcon.map { it as SignalIconModel.Cellular }.collectLastValue()
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = false
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(false)
- assertThat(latest?.level).isEqualTo(0)
- assertThat(latest?.showExclamationMark).isTrue()
+ assertThat(latest?.level).isEqualTo(0)
+ assertThat(latest?.showExclamationMark).isTrue()
- // Changing the level doesn't overwrite the disabled state
- connectionRepository.primaryLevel.value = 2
- assertThat(latest?.level).isEqualTo(0)
- assertThat(latest?.showExclamationMark).isTrue()
+ // Changing the level doesn't overwrite the disabled state
+ connectionRepo.primaryLevel.setValue(2)
+ assertThat(latest?.level).isEqualTo(0)
+ assertThat(latest?.showExclamationMark).isTrue()
- // Once back in service, the regular icon appears
- connectionRepository.isInService.value = true
- assertThat(latest?.level).isEqualTo(2)
- assertThat(latest?.showExclamationMark).isFalse()
-
- job.cancel()
- }
+ // Once back in service, the regular icon appears
+ connectionRepo.isInService.setValue(true)
+ assertThat(latest?.level).isEqualTo(2)
+ assertThat(latest?.showExclamationMark).isFalse()
+ }
@Test
- fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() =
- testScope.runTest {
- var latest: SignalIconModel.Cellular? = null
- val job =
- underTest.signalLevelIcon
- .onEach { latest = it as? SignalIconModel.Cellular? }
- .launchIn(this)
-
- connectionRepository.isNonTerrestrial.value = false
- connectionRepository.isInService.value = true
- connectionRepository.carrierNetworkChangeActive.value = true
- connectionRepository.primaryLevel.value = 1
- connectionRepository.cdmaLevel.value = 1
+ fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = runTest {
+ val latest by
+ underTest.signalLevelIcon.map { it as SignalIconModel.Cellular }.collectLastValue()
- assertThat(latest!!.level).isEqualTo(1)
- assertThat(latest!!.carrierNetworkChange).isTrue()
+ connectionRepo.isNonTerrestrial.setValue(false)
+ connectionRepo.isInService.setValue(true)
+ connectionRepo.carrierNetworkChangeActive.setValue(true)
+ connectionRepo.primaryLevel.setValue(1)
+ connectionRepo.cdmaLevel.setValue(1)
- // SignalIconModel respects the current level
- connectionRepository.primaryLevel.value = 2
+ assertThat(latest!!.level).isEqualTo(1)
+ assertThat(latest!!.carrierNetworkChange).isTrue()
- assertThat(latest!!.level).isEqualTo(2)
- assertThat(latest!!.carrierNetworkChange).isTrue()
+ // SignalIconModel respects the current level
+ connectionRepo.primaryLevel.setValue(2)
- job.cancel()
- }
+ assertThat(latest!!.level).isEqualTo(2)
+ assertThat(latest!!.carrierNetworkChange).isTrue()
+ }
@Test
- fun satBasedIcon_isUsedWhenNonTerrestrial() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_isUsedWhenNonTerrestrial() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // Start off using cellular
- assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java)
+ // Start off using cellular
+ assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java)
- connectionRepository.isNonTerrestrial.value = true
+ connectionRepo.isNonTerrestrial.setValue(true)
- assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java)
- }
+ assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java)
+ }
@DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
// See b/346904529 for more context
- fun satBasedIcon_doesNotInflateSignalStrength_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_doesNotInflateSignalStrength_flagOff() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
- // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
- connectionRepository.inflateSignalStrength.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
+ // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+ connectionRepo.inflateSignalStrength.setValue(true)
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ connectionRepo.primaryLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
+ connectionRepo.inflateSignalStrength.setValue(true)
+ connectionRepo.primaryLevel.setValue(4)
- // Icon level is unaffected
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // Icon level is unaffected
+ assertThat(latest!!.level).isEqualTo(4)
+ }
@EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
// See b/346904529 for more context
- fun satBasedIcon_doesNotInflateSignalStrength_flagOn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_doesNotInflateSignalStrength_flagOn() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
- // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
- connectionRepository.inflateSignalStrength.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
+ // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+ connectionRepo.inflateSignalStrength.setValue(true)
- connectionRepository.satelliteLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ connectionRepo.satelliteLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- connectionRepository.inflateSignalStrength.value = true
- connectionRepository.primaryLevel.value = 4
+ connectionRepo.inflateSignalStrength.setValue(true)
+ connectionRepo.primaryLevel.setValue(4)
- // Icon level is unaffected
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // Icon level is unaffected
+ assertThat(latest!!.level).isEqualTo(4)
+ }
@DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
- fun satBasedIcon_usesPrimaryLevel_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_usesPrimaryLevel_flagOff() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
- // GIVEN primary level is set
- connectionRepository.primaryLevel.value = 4
- connectionRepository.satelliteLevel.value = 0
+ // GIVEN primary level is set
+ connectionRepo.primaryLevel.setValue(4)
+ connectionRepo.satelliteLevel.setValue(0)
- // THEN icon uses the primary level because the flag is off
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // THEN icon uses the primary level because the flag is off
+ assertThat(latest!!.level).isEqualTo(4)
+ }
@EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
- fun satBasedIcon_usesSatelliteLevel_flagOn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
+ fun satBasedIcon_usesSatelliteLevel_flagOn() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
- // GIVEN satellite level is set
- connectionRepository.satelliteLevel.value = 4
- connectionRepository.primaryLevel.value = 0
+ // GIVEN satellite level is set
+ connectionRepo.satelliteLevel.setValue(4)
+ connectionRepo.primaryLevel.setValue(0)
- // THEN icon uses the satellite level because the flag is on
- assertThat(latest!!.level).isEqualTo(4)
- }
+ // THEN icon uses the satellite level because the flag is on
+ assertThat(latest!!.level).isEqualTo(4)
+ }
/**
* Context (b/377518113), this test will not be needed after FLAG_CARRIER_ROAMING_NB_IOT_NTN is
@@ -816,43 +728,23 @@ class MobileIconInteractorKairosTest : SysuiTestCase() {
*/
@DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
@Test
- fun satBasedIcon_reportsLevelZeroWhenOutOfService() =
- testScope.runTest {
- val latest by collectLastValue(underTest.signalLevelIcon)
-
- // GIVEN a satellite connection
- connectionRepository.isNonTerrestrial.value = true
- // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
- connectionRepository.inflateSignalStrength.value = true
+ fun satBasedIcon_reportsLevelZeroWhenOutOfService() = runTest {
+ val latest by underTest.signalLevelIcon.collectLastValue()
- connectionRepository.primaryLevel.value = 4
- assertThat(latest!!.level).isEqualTo(4)
+ // GIVEN a satellite connection
+ connectionRepo.isNonTerrestrial.setValue(true)
+ // GIVEN this carrier has set INFLATE_SIGNAL_STRENGTH
+ connectionRepo.inflateSignalStrength.setValue(true)
- connectionRepository.isInService.value = false
- connectionRepository.primaryLevel.value = 4
+ connectionRepo.primaryLevel.setValue(4)
+ assertThat(latest!!.level).isEqualTo(4)
- // THEN level reports 0, by policy
- assertThat(latest!!.level).isEqualTo(0)
- }
+ connectionRepo.isInService.setValue(false)
+ connectionRepo.primaryLevel.setValue(4)
- private fun createInteractor(
- overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
- ) =
- MobileIconInteractorKairosImpl(
- testScope.backgroundScope,
- mobileIconsInteractor.activeDataConnectionHasDataEnabled,
- mobileIconsInteractor.alwaysShowDataRatIcon,
- mobileIconsInteractor.alwaysUseCdmaLevel,
- mobileIconsInteractor.isSingleCarrier,
- mobileIconsInteractor.mobileIsDefault,
- mobileIconsInteractor.defaultMobileIconMapping,
- mobileIconsInteractor.defaultMobileIconGroup,
- mobileIconsInteractor.isDefaultConnectionFailed,
- mobileIconsInteractor.isForceHidden,
- connectionRepository,
- context,
- overrides,
- )
+ // THEN level reports 0, by policy
+ assertThat(latest!!.level).isEqualTo(0)
+ }
companion object {
private const val GSM_LEVEL = 1
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt
index a9360d139a3d..4f6439d2d0b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,186 +28,164 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fake
import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.runKairosTest
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.core.NewStatusBarIcons
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryLogbufferName
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.fake
-import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
import com.android.systemui.testKosmos
-import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.carrierConfigTracker
import com.google.common.truth.Truth.assertThat
import java.util.UUID
import kotlinx.coroutines.test.advanceTimeBy
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileIconsInteractorKairosTest : SysuiTestCase() {
- private val kosmos by lazy {
+
+ private val kosmos =
testKosmos().apply {
- mobileConnectionsRepositoryLogbufferName = "MobileIconsInteractorTest"
- mobileConnectionsRepository.fake.run {
- setMobileConnectionRepositoryMap(
- mapOf(
- SUB_1_ID to FakeMobileConnectionRepository(SUB_1_ID, mock()),
- SUB_2_ID to FakeMobileConnectionRepository(SUB_2_ID, mock()),
- SUB_3_ID to FakeMobileConnectionRepository(SUB_3_ID, mock()),
- SUB_4_ID to FakeMobileConnectionRepository(SUB_4_ID, mock()),
- )
- )
- setActiveMobileDataSubscriptionId(SUB_1_ID)
- }
+ useUnconfinedTestDispatcher()
+ mobileConnectionsRepositoryKairos =
+ fakeMobileConnectionsRepositoryKairos.apply {
+ setActiveMobileDataSubscriptionId(SUB_1_ID)
+ subscriptions.setValue(listOf(SUB_1, SUB_2, SUB_3_OPP, SUB_4_OPP))
+ }
featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
}
- }
- // shortcut rename
- private val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake }
-
- private val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() }
-
- private val Kosmos.underTest by Fixture {
- MobileIconsInteractorKairosImpl(
- mobileConnectionsRepository,
- carrierConfigTracker,
- tableLogger = mock(),
- connectivityRepository,
- FakeUserSetupRepository(),
- testScope.backgroundScope,
- context,
- featureFlagsClassic,
- )
- }
+ private val Kosmos.underTest
+ get() = mobileIconsInteractorKairos
+
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
@Test
- fun filteredSubscriptions_default() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ fun filteredSubscriptions_default() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(emptyList())
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
- }
+ assertThat(latest).isEqualTo(emptyList<SubscriptionModel>())
+ }
// Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
@Test
- fun filteredSubscriptions_moreThanTwo_doesNotFilter() =
- kosmos.runTest {
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
+ fun filteredSubscriptions_moreThanTwo_doesNotFilter() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(SUB_1, SUB_3_OPP, SUB_4_OPP)
+ )
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_4_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
- }
+ assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
+ }
@Test
- fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
- kosmos.runTest {
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+ fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
- }
+ assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+ }
@Test
- fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() =
- kosmos.runTest {
- connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() = runTest {
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_3_OPP, SUB_4_OPP))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
- }
+ assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
+ }
@Test
- fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() =
- kosmos.runTest {
- val (sub1, sub2) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_1_ID, SUB_2_ID),
- opportunistic = Pair(true, true),
- grouped = false,
- )
- connectionsRepository.setSubscriptions(listOf(sub1, sub2))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+ fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() = runTest {
+ val (sub1, sub2) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_1_ID, SUB_2_ID),
+ opportunistic = Pair(true, true),
+ grouped = false,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_1_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub1, sub2))
- }
+ assertThat(latest).isEqualTo(listOf(sub1, sub2))
+ }
@Test
- fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() =
- kosmos.runTest {
- val (sub3, sub4) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub3, sub4))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() = runTest {
+ val (sub3, sub4) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub3, sub4))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- // Filtered subscriptions should show the active one when the config is false
- assertThat(latest).isEqualTo(listOf(sub3))
- }
+ // Filtered subscriptions should show the active one when the config is false
+ assertThat(latest).isEqualTo(listOf(sub3))
+ }
@Test
- fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() =
- kosmos.runTest {
- val (sub3, sub4) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub3, sub4))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() = runTest {
+ val (sub3, sub4) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub3, sub4))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_4_ID)
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- // Filtered subscriptions should show the active one when the config is false
- assertThat(latest).isEqualTo(listOf(sub4))
- }
+ // Filtered subscriptions should show the active one when the config is false
+ assertThat(latest).isEqualTo(listOf(sub4))
+ }
@Test
fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_active_1() =
- kosmos.runTest {
+ runTest {
val (sub1, sub3) =
createSubscriptionPair(
subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
opportunistic = Pair(false, true),
grouped = true,
)
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_1_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
@@ -216,19 +194,19 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_nonActive_1() =
- kosmos.runTest {
+ runTest {
val (sub1, sub3) =
createSubscriptionPair(
subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
opportunistic = Pair(false, true),
grouped = true,
)
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
@@ -236,135 +214,130 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
}
@Test
- fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() =
- kosmos.runTest {
- val (sub1, sub3) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() = runTest {
+ val (sub1, sub3) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub3))
- }
+ assertThat(latest).isEqualTo(listOf(sub3))
+ }
@Test
- fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() =
- kosmos.runTest {
- val (sub1, sub3) =
- createSubscriptionPair(
- subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
- opportunistic = Pair(true, true),
- grouped = true,
- )
- connectionsRepository.setSubscriptions(listOf(sub1, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID
- whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
- .thenReturn(false)
+ fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() = runTest {
+ val (sub1, sub3) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_1_ID, SUB_3_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID
+ whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
+ .thenReturn(false)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub1))
- }
+ assertThat(latest).isEqualTo(listOf(sub1))
+ }
@Test
- fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() =
- kosmos.runTest {
- // GIVEN the flag is false
- featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false)
+ fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() = runTest {
+ // GIVEN the flag is false
+ featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false)
- // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING
- val sub1 =
- SubscriptionModel(
- subscriptionId = SUB_1_ID,
- isOpportunistic = false,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_PROVISIONING,
- )
+ // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING
+ val sub1 =
+ SubscriptionModel(
+ subscriptionId = SUB_1_ID,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_PROVISIONING,
+ )
- connectionsRepository.setSubscriptions(listOf(sub1))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1))
- // WHEN filtering is applied
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ // WHEN filtering is applied
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- // THEN the provisioning sub is still present (unfiltered)
- assertThat(latest).isEqualTo(listOf(sub1))
- }
+ // THEN the provisioning sub is still present (unfiltered)
+ assertThat(latest).isEqualTo(listOf(sub1))
+ }
@Test
- fun filteredSubscriptions_filtersOutProvisioningSubs() =
- kosmos.runTest {
- val sub1 =
- SubscriptionModel(
- subscriptionId = SUB_1_ID,
- isOpportunistic = false,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_UNSET,
- )
- val sub2 =
- SubscriptionModel(
- subscriptionId = SUB_2_ID,
- isOpportunistic = false,
- carrierName = "Carrier 2",
- profileClass = PROFILE_CLASS_PROVISIONING,
- )
+ fun filteredSubscriptions_filtersOutProvisioningSubs() = runTest {
+ val sub1 =
+ SubscriptionModel(
+ subscriptionId = SUB_1_ID,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val sub2 =
+ SubscriptionModel(
+ subscriptionId = SUB_2_ID,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_PROVISIONING,
+ )
- connectionsRepository.setSubscriptions(listOf(sub1, sub2))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2))
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(sub1))
- }
+ assertThat(latest).isEqualTo(listOf(sub1))
+ }
/** Note: I'm not sure if this will ever be the case, but we can test it at least */
@Test
- fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() =
- kosmos.runTest {
- // This is a contrived test case, where the active subId is the one that would
- // also be filtered by opportunistic filtering.
-
- // GIVEN grouped, opportunistic subscriptions
- val groupUuid = ParcelUuid(UUID.randomUUID())
- val sub1 =
- SubscriptionModel(
- subscriptionId = 1,
- isOpportunistic = true,
- groupUuid = groupUuid,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_PROVISIONING,
- )
+ fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() = runTest {
+ // This is a contrived test case, where the active subId is the one that would
+ // also be filtered by opportunistic filtering.
- val sub2 =
- SubscriptionModel(
- subscriptionId = 2,
- isOpportunistic = true,
- groupUuid = groupUuid,
- carrierName = "Carrier 2",
- profileClass = PROFILE_CLASS_UNSET,
- )
+ // GIVEN grouped, opportunistic subscriptions
+ val groupUuid = ParcelUuid(UUID.randomUUID())
+ val sub1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = true,
+ groupUuid = groupUuid,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_PROVISIONING,
+ )
- // GIVEN active subId is 1
- connectionsRepository.setSubscriptions(listOf(sub1, sub2))
- connectionsRepository.setActiveMobileDataSubscriptionId(1)
+ val sub2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = true,
+ groupUuid = groupUuid,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
- // THEN filtering of provisioning subs takes place first, and we result in sub2
+ // GIVEN active subId is 1
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(1)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ // THEN filtering of provisioning subs takes place first, and we result in sub2
- assertThat(latest).isEqualTo(listOf(sub2))
- }
+ val latest by underTest.filteredSubscriptions.collectLastValue()
+
+ assertThat(latest).isEqualTo(listOf(sub2))
+ }
@Test
fun filteredSubscriptions_groupedPairAndNonProvisioned_groupedFilteringStillHappens() =
- kosmos.runTest {
+ runTest {
// Grouped filtering only happens when the list of subs is length 2. In this case
// we'll show that filtering of provisioning subs happens before, and thus grouped
// filtering happens even though the unfiltered list is length 3
@@ -384,87 +357,88 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
profileClass = PROFILE_CLASS_PROVISIONING,
)
- connectionsRepository.setSubscriptions(listOf(sub1, sub2, sub3))
- connectionsRepository.setActiveMobileDataSubscriptionId(1)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(sub1, sub2, sub3))
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(1)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
assertThat(latest).isEqualTo(listOf(sub1))
}
@Test
- fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() =
- kosmos.runTest {
- val notExclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = false,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
+ fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() = runTest {
+ val notExclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
- connectionsRepository.setSubscriptions(listOf(notExclusivelyNonTerrestrialSub))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(notExclusivelyNonTerrestrialSub)
+ )
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
- }
+ assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
+ }
@Test
- fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() =
- kosmos.runTest {
- val exclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = true,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
+ fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() = runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
- connectionsRepository.setSubscriptions(listOf(exclusivelyNonTerrestrialSub))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(exclusivelyNonTerrestrialSub)
+ )
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEmpty()
- }
+ assertThat(latest).isEmpty()
+ }
@Test
- fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() =
- kosmos.runTest {
- val exclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = true,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
- val otherSub1 =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = false,
- subscriptionId = 1,
- carrierName = "Carrier 1",
- profileClass = PROFILE_CLASS_UNSET,
- )
- val otherSub2 =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = false,
- subscriptionId = 2,
- carrierName = "Carrier 2",
- profileClass = PROFILE_CLASS_UNSET,
- )
-
- connectionsRepository.setSubscriptions(
- listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+ fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() = runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub1 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 1,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub2 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 2,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+ )
+
+ val latest by underTest.filteredSubscriptions.collectLastValue()
- assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
- }
+ assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
+ }
@Test
fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() =
- kosmos.runTest {
+ runTest {
// Exclusively non-terrestrial sub
val exclusivelyNonTerrestrialSub =
SubscriptionModel(
@@ -483,10 +457,12 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
)
// WHEN both an exclusively non-terrestrial sub and opportunistic sub pair is included
- connectionsRepository.setSubscriptions(listOf(sub3, sub4, exclusivelyNonTerrestrialSub))
- connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(sub3, sub4, exclusivelyNonTerrestrialSub)
+ )
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(SUB_3_ID)
- val latest by collectLastValue(underTest.filteredSubscriptions)
+ val latest by underTest.filteredSubscriptions.collectLastValue()
// THEN both the only-non-terrestrial sub and the non-active sub are filtered out,
// leaving only sub3.
@@ -494,484 +470,427 @@ class MobileIconsInteractorKairosTest : SysuiTestCase() {
}
@Test
- fun activeDataConnection_turnedOn() =
- kosmos.runTest {
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .dataEnabled
- .value = true
+ fun activeDataConnection_turnedOn() = runTest {
+ val connection1 =
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[SUB_1_ID]!!
- val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
+ connection1.dataEnabled.setValue(true)
- assertThat(latest).isTrue()
- }
+ val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue()
+
+ assertThat(latest).isTrue()
+ }
@Test
- fun activeDataConnection_turnedOff() =
- kosmos.runTest {
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .dataEnabled
- .value = true
+ fun activeDataConnection_turnedOff() = runTest {
+ val connection1 =
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[SUB_1_ID]!!
- val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
+ connection1.dataEnabled.setValue(true)
+ val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue()
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .dataEnabled
- .value = false
+ connection1.dataEnabled.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun activeDataConnection_invalidSubId() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
+ fun activeDataConnection_invalidSubId() = runTest {
+ val latest by underTest.activeDataConnectionHasDataEnabled.collectLastValue()
- connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
+ mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(
+ INVALID_SUBSCRIPTION_ID
+ )
- // An invalid active subId should tell us that data is off
- assertThat(latest).isFalse()
- }
+ // An invalid active subId should tell us that data is off
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_default_validated_notFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_default_validated_notFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_notDefault_notValidated_notFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_notDefault_notValidated_notFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = false
- connectionsRepository.defaultConnectionIsValidated.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_default_notValidated_failed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_default_notValidated_failed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun failedConnection_carrierMergedDefault_notValidated_failed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_carrierMergedDefault_notValidated_failed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.hasCarrierMergedConnection.value = true
- connectionsRepository.defaultConnectionIsValidated.value = false
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
/** Regression test for b/275076959. */
@Test
- fun failedConnection_dataSwitchInSameGroup_notFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_dataSwitchInSameGroup_notFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- // WHEN there's a data change in the same subscription group
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
+ // WHEN there's a data change in the same subscription group
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- // THEN the default connection is *not* marked as failed because of forced validation
- assertThat(latest).isFalse()
- }
+ // THEN the default connection is *not* marked as failed because of forced validation
+ assertThat(latest).isFalse()
+ }
@Test
- fun failedConnection_dataSwitchNotInSameGroup_isFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun failedConnection_dataSwitchNotInSameGroup_isFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- // WHEN the connection is invalidated without a activeSubChangedInGroupEvent
- connectionsRepository.defaultConnectionIsValidated.value = false
+ // WHEN the connection is invalidated without a activeSubChangedInGroupEvent
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
- // THEN the connection is immediately marked as failed
- assertThat(latest).isTrue()
- }
+ // THEN the connection is immediately marked as failed
+ assertThat(latest).isTrue()
+ }
@Test
- fun alwaysShowDataRatIcon_configHasTrue() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
+ fun alwaysShowDataRatIcon_configHasTrue() = runTest {
+ val latest by underTest.alwaysShowDataRatIcon.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowDataRatIcon = true
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowDataRatIcon = true
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun alwaysShowDataRatIcon_configHasFalse() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
+ fun alwaysShowDataRatIcon_configHasFalse() = runTest {
+ val latest by underTest.alwaysShowDataRatIcon.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowDataRatIcon = false
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowDataRatIcon = false
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun alwaysUseCdmaLevel_configHasTrue() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
+ fun alwaysUseCdmaLevel_configHasTrue() = runTest {
+ val latest by underTest.alwaysUseCdmaLevel.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowCdmaRssi = true
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowCdmaRssi = true
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun alwaysUseCdmaLevel_configHasFalse() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
+ fun alwaysUseCdmaLevel_configHasFalse() = runTest {
+ val latest by underTest.alwaysUseCdmaLevel.collectLastValue()
- val config = MobileMappings.Config()
- config.alwaysShowCdmaRssi = false
- connectionsRepository.defaultDataSubRatConfig.value = config
+ val config = MobileMappings.Config()
+ config.alwaysShowCdmaRssi = false
+ mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue(config)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun isSingleCarrier_zeroSubscriptions_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_zeroSubscriptions_false() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(emptyList())
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(emptyList())
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun isSingleCarrier_oneSubscription_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_oneSubscription_true() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1))
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun isSingleCarrier_twoSubscriptions_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_twoSubscriptions_false() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun isSingleCarrier_updates() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isSingleCarrier)
+ fun isSingleCarrier_updates() = runTest {
+ val latest by underTest.isSingleCarrier.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1))
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1))
+ assertThat(latest).isTrue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- assertThat(latest).isFalse()
- }
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ assertThat(latest).isFalse()
+ }
@Test
- fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = false
- connectionsRepository.hasCarrierMergedConnection.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(false)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.hasCarrierMergedConnection.value = false
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(false)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
/** Regression test for b/272586234. */
@Test
- fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = false
- connectionsRepository.hasCarrierMergedConnection.value = true
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun mobileIsDefault_updatesWhenRepoUpdates() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.mobileIsDefault)
+ fun mobileIsDefault_updatesWhenRepoUpdates() = runTest {
+ val latest by underTest.mobileIsDefault.collectLastValue()
- connectionsRepository.mobileIsDefault.value = true
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ assertThat(latest).isTrue()
- connectionsRepository.mobileIsDefault.value = false
- assertThat(latest).isFalse()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false)
+ assertThat(latest).isFalse()
- connectionsRepository.hasCarrierMergedConnection.value = true
- assertThat(latest).isTrue()
- }
+ mobileConnectionsRepositoryKairos.fake.hasCarrierMergedConnection.setValue(true)
+ assertThat(latest).isTrue()
+ }
// The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow
// is private and can only be tested by looking at [isDefaultConnectionFailed].
@Test
- fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
-
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
-
- // Trigger a data change in the same subscription group that's not yet validated
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
-
- // After 1s, the force validation bit is still present, so the connection is not marked
- // as failed
- testScope.advanceTimeBy(1000)
- assertThat(latest).isFalse()
-
- // After 2s, the force validation expires so the connection updates to failed
- testScope.advanceTimeBy(1001)
- assertThat(latest).isTrue()
- }
+ fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- @Test
- fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
+ // Trigger a data change in the same subscription group that's not yet validated
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ // After 1s, the force validation bit is still present, so the connection is not marked
+ // as failed
+ testScope.advanceTimeBy(1000)
+ assertThat(latest).isFalse()
- assertThat(latest).isTrue()
- }
+ // After 2s, the force validation expires so the connection updates to failed
+ testScope.advanceTimeBy(1001)
+ assertThat(latest).isTrue()
+ }
@Test
- fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+ fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- // GIVEN the network starts validated
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- // WHEN a data change happens in the same group
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- // WHEN the validation bit is lost
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
-
- // WHEN another data change happens in the same group
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
-
- // THEN the forced validation bit is still used...
- assertThat(latest).isFalse()
-
- testScope.advanceTimeBy(1000)
- assertThat(latest).isFalse()
-
- // ... but expires after 2s
- testScope.advanceTimeBy(1001)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun dataSwitch_whileAlreadyForcingValidation_resetsClock() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDefaultConnectionFailed)
- connectionsRepository.mobileIsDefault.value = true
- connectionsRepository.defaultConnectionIsValidated.value = true
- runCurrent()
+ fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
+ // GIVEN the network starts validated
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- testScope.advanceTimeBy(1000)
+ // WHEN a data change happens in the same group
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- // WHEN another change in same group event happens
- connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- connectionsRepository.defaultConnectionIsValidated.value = false
- runCurrent()
+ // WHEN the validation bit is lost
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- // THEN the forced validation remains for exactly 2 more seconds from now
+ // WHEN another data change happens in the same group
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- // 1.500s from second event
- testScope.advanceTimeBy(1500)
- assertThat(latest).isFalse()
+ // THEN the forced validation bit is still used...
+ assertThat(latest).isFalse()
- // 2.001s from the second event
- testScope.advanceTimeBy(501)
- assertThat(latest).isTrue()
- }
+ testScope.advanceTimeBy(1000)
+ assertThat(latest).isFalse()
- @Test
- fun isForceHidden_repoHasMobileHidden_true() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isForceHidden)
+ // ... but expires after 2s
+ testScope.advanceTimeBy(1001)
+ assertThat(latest).isTrue()
+ }
- kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+ @Test
+ fun dataSwitch_whileAlreadyForcingValidation_resetsClock() = runTest {
+ val latest by underTest.isDefaultConnectionFailed.collectLastValue()
+ mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(true)
+ runCurrent()
- assertThat(latest).isTrue()
- }
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
- @Test
- fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isForceHidden)
+ testScope.advanceTimeBy(1000)
- kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
+ // WHEN another change in same group event happens
+ mobileConnectionsRepositoryKairos.fake.activeSubChangedInGroupEvent.emit(Unit)
+ mobileConnectionsRepositoryKairos.fake.defaultConnectionIsValidated.setValue(false)
+ runCurrent()
- assertThat(latest).isFalse()
- }
+ // THEN the forced validation remains for exactly 2 more seconds from now
- @Test
- fun iconInteractor_cachedPerSubId() =
- kosmos.runTest {
- val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID)
- val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID)
+ // 1.500s from second event
+ testScope.advanceTimeBy(1500)
+ assertThat(latest).isFalse()
- assertThat(interactor1).isNotNull()
- assertThat(interactor1).isSameInstanceAs(interactor2)
- }
+ // 2.001s from the second event
+ testScope.advanceTimeBy(501)
+ assertThat(latest).isTrue()
+ }
@Test
- fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode)
+ fun isForceHidden_repoHasMobileHidden_true() = runTest {
+ val latest by underTest.isForceHidden.collectLastValue()
- connectionsRepository.isDeviceEmergencyCallCapable.value = true
+ kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun isForceHidden_repoDoesNotHaveMobileHidden_false() = runTest {
+ val latest by underTest.isForceHidden.collectLastValue()
- connectionsRepository.isDeviceEmergencyCallCapable.value = false
+ kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun defaultDataSubId_tracksRepo() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.defaultDataSubId)
+ fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() = runTest {
+ val latest by underTest.isDeviceInEmergencyCallsOnlyMode.collectLastValue()
- connectionsRepository.defaultDataSubId.value = 1
+ mobileConnectionsRepositoryKairos.fake.isDeviceEmergencyCallCapable.setValue(true)
- assertThat(latest).isEqualTo(1)
+ assertThat(latest).isTrue()
- connectionsRepository.defaultDataSubId.value = 2
+ mobileConnectionsRepositoryKairos.fake.isDeviceEmergencyCallCapable.setValue(false)
- assertThat(latest).isEqualTo(2)
- }
+ assertThat(latest).isFalse()
+ }
@Test
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun isStackable_tracksNumberOfSubscriptions() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isStackable)
+ fun isStackable_tracksNumberOfSubscriptions() = runTest {
+ val latest by underTest.isStackable.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1))
- assertThat(latest).isFalse()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1))
+ assertThat(latest).isFalse()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ assertThat(latest).isTrue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2, SUB_3_OPP))
- assertThat(latest).isFalse()
- }
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(
+ listOf(SUB_1, SUB_2, SUB_3_OPP)
+ )
+ assertThat(latest).isFalse()
+ }
@Test
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun isStackable_checksForTerrestrialConnections() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isStackable)
+ fun isStackable_checksForTerrestrialConnections() = runTest {
+ val latest by underTest.isStackable.collectLastValue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- setNumberOfLevelsForSubId(SUB_1_ID, 5)
- setNumberOfLevelsForSubId(SUB_2_ID, 5)
- assertThat(latest).isTrue()
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ setNumberOfLevelsForSubId(SUB_1_ID, 5)
+ setNumberOfLevelsForSubId(SUB_2_ID, 5)
+ assertThat(latest).isTrue()
- (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
- as FakeMobileConnectionRepository)
- .isNonTerrestrial
- .value = true
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId
+ .sample()[SUB_1_ID]!!
+ .isNonTerrestrial
+ .setValue(true)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
- fun isStackable_checksForNumberOfBars() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.isStackable)
+ fun isStackable_checksForNumberOfBars() = runTest {
+ val latest by underTest.isStackable.collectLastValue()
- // Number of levels is the same for both
- connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- setNumberOfLevelsForSubId(SUB_1_ID, 5)
- setNumberOfLevelsForSubId(SUB_2_ID, 5)
+ // Number of levels is the same for both
+ mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2))
+ setNumberOfLevelsForSubId(SUB_1_ID, 5)
+ setNumberOfLevelsForSubId(SUB_2_ID, 5)
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
- // Change the number of levels to be different than SUB_2
- setNumberOfLevelsForSubId(SUB_1_ID, 6)
+ // Change the number of levels to be different than SUB_2
+ setNumberOfLevelsForSubId(SUB_1_ID, 6)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
- private fun setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) {
- with(kosmos) {
- (fakeMobileConnectionsRepository.getRepoForSubId(subId)
- as FakeMobileConnectionRepository)
- .numberOfLevels
- .value = numberOfLevels
- }
+ private suspend fun KairosTestScope.setNumberOfLevelsForSubId(subId: Int, numberOfLevels: Int) {
+ mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId
+ .sample()[subId]!!
+ .numberOfLevels
+ .setValue(numberOfLevels)
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt
new file mode 100644
index 000000000000..57e63a595b8f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class LocationBasedMobileIconViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var commonImpl: MobileIconViewModelCommonKairos
+ private lateinit var homeIcon: HomeMobileIconViewModelKairos
+ private lateinit var qsIcon: QsMobileIconViewModelKairos
+ private lateinit var keyguardIcon: KeyguardMobileIconViewModelKairos
+ private lateinit var iconsInteractor: MobileIconsInteractor
+ private lateinit var interactor: MobileIconInteractor
+ private val connectionsRepository = kosmos.fakeMobileConnectionsRepository
+ private lateinit var repository: FakeMobileConnectionRepository
+ private lateinit var airplaneModeInteractor: AirplaneModeInteractor
+
+ private val connectivityRepository = FakeConnectivityRepository()
+ private val flags =
+ FakeFeatureFlagsClassic().also {
+ it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+ }
+
+ @Mock private lateinit var constants: ConnectivityConstants
+ private val tableLogBuffer =
+ logcatTableLogBuffer(kosmos, "LocationBasedMobileIconViewModelTest")
+ @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
+ connectionsRepository,
+ )
+ repository =
+ FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply {
+ isInService.value = true
+ cdmaLevel.value = 1
+ primaryLevel.value = 1
+ isEmergencyOnly.value = false
+ numberOfLevels.value = 4
+ resolvedNetworkType.value = ResolvedNetworkType.DefaultNetworkType(lookupKey = "3G")
+ dataConnectionState.value = DataConnectionState.Connected
+ }
+
+ connectionsRepository.activeMobileDataRepository.value = repository
+
+ connectivityRepository.apply { setMobileConnected() }
+
+ iconsInteractor =
+ MobileIconsInteractorImpl(
+ connectionsRepository,
+ carrierConfigTracker,
+ tableLogBuffer,
+ connectivityRepository,
+ FakeUserSetupRepository(),
+ testScope.backgroundScope,
+ context,
+ flags,
+ )
+
+ interactor =
+ MobileIconInteractorImpl(
+ testScope.backgroundScope,
+ iconsInteractor.activeDataConnectionHasDataEnabled,
+ iconsInteractor.alwaysShowDataRatIcon,
+ iconsInteractor.alwaysUseCdmaLevel,
+ iconsInteractor.isSingleCarrier,
+ iconsInteractor.mobileIsDefault,
+ iconsInteractor.defaultMobileIconMapping,
+ iconsInteractor.defaultMobileIconGroup,
+ iconsInteractor.isDefaultConnectionFailed,
+ iconsInteractor.isForceHidden,
+ repository,
+ context,
+ MobileIconCarrierIdOverridesFake(),
+ )
+
+ commonImpl =
+ MobileIconViewModelKairos(
+ SUB_1_ID,
+ interactor,
+ airplaneModeInteractor,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ homeIcon = HomeMobileIconViewModelKairos(commonImpl, mock())
+ qsIcon = QsMobileIconViewModelKairos(commonImpl)
+ keyguardIcon = KeyguardMobileIconViewModelKairos(commonImpl)
+ }
+
+ @Test
+ fun locationBasedViewModelsReceiveSameIconIdWhenCommonImplUpdates() =
+ testScope.runTest {
+ var latestHome: SignalIconModel? = null
+ val homeJob = homeIcon.icon.onEach { latestHome = it }.launchIn(this)
+
+ var latestQs: SignalIconModel? = null
+ val qsJob = qsIcon.icon.onEach { latestQs = it }.launchIn(this)
+
+ var latestKeyguard: SignalIconModel? = null
+ val keyguardJob = keyguardIcon.icon.onEach { latestKeyguard = it }.launchIn(this)
+
+ var expected = defaultSignal(level = 1)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ repository.setAllLevels(2)
+ expected = defaultSignal(level = 2)
+
+ assertThat(latestHome).isEqualTo(expected)
+ assertThat(latestQs).isEqualTo(expected)
+ assertThat(latestKeyguard).isEqualTo(expected)
+
+ homeJob.cancel()
+ qsJob.cancel()
+ keyguardJob.cancel()
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+ private const val NUM_LEVELS = 4
+
+ /** Convenience constructor for these tests */
+ fun defaultSignal(level: Int = 1): SignalIconModel {
+ return SignalIconModel.Cellular(
+ level,
+ NUM_LEVELS,
+ showExclamationMark = false,
+ carrierNetworkChange = false,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt
new file mode 100644
index 000000000000..6b114a8256f2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt
@@ -0,0 +1,1077 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.TelephonyIcons.G
+import com.android.settingslib.mobile.TelephonyIcons.THREE_G
+import com.android.settingslib.mobile.TelephonyIcons.UNKNOWN
+import com.android.systemui.Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake
+import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository.Companion.DEFAULT_NETWORK_NAME
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MobileIconViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private var connectivityRepository = FakeConnectivityRepository()
+
+ private lateinit var underTest: MobileIconViewModelKairos
+ private lateinit var interactor: MobileIconInteractorImpl
+ private lateinit var iconsInteractor: MobileIconsInteractorImpl
+ private lateinit var repository: FakeMobileConnectionRepository
+ private lateinit var connectionsRepository: FakeMobileConnectionsRepository
+ private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+ private lateinit var airplaneModeInteractor: AirplaneModeInteractor
+ @Mock private lateinit var constants: ConnectivityConstants
+ private val tableLogBuffer = logcatTableLogBuffer(kosmos, "MobileIconViewModelTest")
+ @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
+
+ private val flags =
+ FakeFeatureFlagsClassic().also {
+ it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+ }
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(constants.hasDataCapabilities).thenReturn(true)
+
+ connectionsRepository =
+ FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer)
+
+ repository =
+ FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply {
+ setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ isInService.value = true
+ dataConnectionState.value = DataConnectionState.Connected
+ dataEnabled.value = true
+ }
+ connectionsRepository.activeMobileDataRepository.value = repository
+ connectionsRepository.mobileIsDefault.value = true
+
+ airplaneModeRepository = FakeAirplaneModeRepository()
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ airplaneModeRepository,
+ connectivityRepository,
+ kosmos.fakeMobileConnectionsRepository,
+ )
+
+ iconsInteractor =
+ MobileIconsInteractorImpl(
+ connectionsRepository,
+ carrierConfigTracker,
+ tableLogBuffer,
+ connectivityRepository,
+ FakeUserSetupRepository(),
+ testScope.backgroundScope,
+ context,
+ flags,
+ )
+
+ interactor =
+ MobileIconInteractorImpl(
+ testScope.backgroundScope,
+ iconsInteractor.activeDataConnectionHasDataEnabled,
+ iconsInteractor.alwaysShowDataRatIcon,
+ iconsInteractor.alwaysUseCdmaLevel,
+ iconsInteractor.isSingleCarrier,
+ iconsInteractor.mobileIsDefault,
+ iconsInteractor.defaultMobileIconMapping,
+ iconsInteractor.defaultMobileIconGroup,
+ iconsInteractor.isDefaultConnectionFailed,
+ iconsInteractor.isForceHidden,
+ repository,
+ context,
+ MobileIconCarrierIdOverridesFake(),
+ )
+ createAndSetViewModel()
+ }
+
+ @Test
+ fun isVisible_notDataCapable_alwaysFalse() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.hasDataCapabilities).thenReturn(false)
+ createAndSetViewModel()
+
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_notAirplane_notForceHidden_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(false)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_airplaneAndNotAllowed_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(true)
+ repository.isAllowedDuringAirplaneMode.value = false
+ connectivityRepository.setForceHiddenIcons(setOf())
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ /** Regression test for b/291993542. */
+ @Test
+ fun isVisible_airplaneButAllowed_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(true)
+ repository.isAllowedDuringAirplaneMode.value = true
+ connectivityRepository.setForceHiddenIcons(setOf())
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_forceHidden_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(false)
+ connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_respondsToUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+ airplaneModeRepository.setIsAirplaneMode(false)
+ connectivityRepository.setForceHiddenIcons(setOf())
+
+ assertThat(latest).isTrue()
+
+ airplaneModeRepository.setIsAirplaneMode(true)
+ assertThat(latest).isFalse()
+
+ repository.isAllowedDuringAirplaneMode.value = true
+ assertThat(latest).isTrue()
+
+ connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isVisible_satellite_respectsAirplaneMode() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.isVisible)
+
+ repository.isNonTerrestrial.value = true
+ airplaneModeInteractor.setIsAirplaneMode(false)
+
+ assertThat(latest).isTrue()
+
+ airplaneModeInteractor.setIsAirplaneMode(true)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun contentDescription_notInService_usesNoPhone() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.isInService.value = false
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_includesNetworkName() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.isInService.value = true
+ repository.networkName.value = NetworkNameModel.SubscriptionDerived("Test Network Name")
+ repository.numberOfLevels.value = 5
+ repository.setAllLevels(3)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular("Test Network Name", THREE_BARS))
+ }
+
+ @Test
+ fun contentDescription_inService_usesLevel() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.setAllLevels(2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS))
+
+ repository.setAllLevels(0)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_nonInflated_invalidLevelUsesNoSignalText() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = false
+ repository.setAllLevels(-1)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+
+ repository.setAllLevels(100)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_nonInflated_levelStrings() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = false
+ repository.setAllLevels(0)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+
+ repository.setAllLevels(1)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR))
+
+ repository.setAllLevels(2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS))
+
+ repository.setAllLevels(3)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS))
+
+ repository.setAllLevels(4)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS))
+ }
+
+ @Test
+ fun contentDescription_inflated_invalidLevelUsesNoSignalText() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = true
+ repository.numberOfLevels.value = 6
+
+ repository.setAllLevels(-2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+
+ repository.setAllLevels(100)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL))
+ }
+
+ @Test
+ fun contentDescription_inflated_levelStrings() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = true
+ repository.numberOfLevels.value = 6
+
+ // Note that the _repo_ level is 1 lower than the reported level through the interactor
+
+ repository.setAllLevels(0)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR))
+
+ repository.setAllLevels(1)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS))
+
+ repository.setAllLevels(2)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS))
+
+ repository.setAllLevels(3)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FOUR_BARS))
+
+ repository.setAllLevels(4)
+
+ assertThat(latest as MobileContentDescription.Cellular)
+ .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS))
+ }
+
+ @Test
+ fun contentDescription_nonInflated_testABunchOfLevelsForNull() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+
+ repository.inflateSignalStrength.value = false
+ repository.numberOfLevels.value = 5
+
+ // -1 and 5 are out of the bounds for non-inflated content descriptions
+ for (i in -1..5) {
+ repository.setAllLevels(i)
+ when (i) {
+ -1,
+ 5 ->
+ assertWithMessage("Level $i is expected to be 'no signal'")
+ .that((latest as MobileContentDescription.Cellular).levelDescriptionRes)
+ .isEqualTo(NO_SIGNAL)
+ else ->
+ assertWithMessage("Level $i is expected not to be null")
+ .that(latest)
+ .isNotNull()
+ }
+ }
+ }
+
+ @Test
+ fun contentDescription_inflated_testABunchOfLevelsForNull() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.contentDescription)
+ repository.inflateSignalStrength.value = true
+ repository.numberOfLevels.value = 6
+ // -1 and 6 are out of the bounds for inflated content descriptions
+ // Note that the interactor adds 1 to the reported level, hence the -2 to 5 range
+ for (i in -2..5) {
+ repository.setAllLevels(i)
+ when (i) {
+ -2,
+ 5 ->
+ assertWithMessage("Level $i is expected to be 'no signal'")
+ .that((latest as MobileContentDescription.Cellular).levelDescriptionRes)
+ .isEqualTo(NO_SIGNAL)
+ else ->
+ assertWithMessage("Level $i is not expected to be null")
+ .that(latest)
+ .isNotNull()
+ }
+ }
+ }
+
+ @Test
+ fun networkType_dataEnabled_groupIsRepresented() =
+ testScope.runTest {
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ connectionsRepository.mobileIsDefault.value = true
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_null_whenDisabled() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.setDataEnabled(false)
+ connectionsRepository.mobileIsDefault.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_null_whenCarrierNetworkChangeActive() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.carrierNetworkChangeActive.value = true
+ connectionsRepository.mobileIsDefault.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkTypeIcon_notNull_whenEnabled() =
+ testScope.runTest {
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.setDataEnabled(true)
+ repository.dataConnectionState.value = DataConnectionState.Connected
+ connectionsRepository.mobileIsDefault.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_nullWhenDataDisconnects() =
+ testScope.runTest {
+ val initial =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(initial)
+
+ repository.dataConnectionState.value = DataConnectionState.Disconnected
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_null_changeToDisabled() =
+ testScope.runTest {
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ repository.dataEnabled.value = true
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isEqualTo(expected)
+
+ repository.dataEnabled.value = false
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownEvenWhenDisabled() =
+ testScope.runTest {
+ repository.dataEnabled.value = false
+
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownEvenWhenDisconnected() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.dataConnectionState.value = DataConnectionState.Disconnected
+
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownEvenWhenFailedConnection() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ connectionsRepository.mobileIsDefault.value = true
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_usesDefaultIconWhenInvalid() =
+ testScope.runTest {
+ // The UNKNOWN icon group doesn't have a valid data type icon ID, and the logic from the
+ // old pipeline was to use the default icon group if the map doesn't exist
+ repository.setNetworkTypeKey(UNKNOWN.name)
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ connectionsRepository.defaultMobileIconGroup.value.dataType,
+ ContentDescription.Resource(G.dataContentDescription),
+ )
+
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_alwaysShow_shownWhenNotDefault() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ connectionsRepository.mobileIsDefault.value = false
+ connectionsRepository.defaultDataSubRatConfig.value =
+ MobileMappings.Config().also { it.alwaysShowDataRatIcon = true }
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ val expected =
+ Icon.Resource(
+ THREE_G.dataType,
+ ContentDescription.Resource(THREE_G.dataContentDescription),
+ )
+ assertThat(latest).isEqualTo(expected)
+
+ job.cancel()
+ }
+
+ @Test
+ fun networkType_notShownWhenNotDefault() =
+ testScope.runTest {
+ repository.setNetworkTypeKey(connectionsRepository.GSM_KEY)
+ repository.dataConnectionState.value = DataConnectionState.Connected
+ connectionsRepository.mobileIsDefault.value = false
+
+ var latest: Icon? = null
+ val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun roaming() =
+ testScope.runTest {
+ repository.setAllRoaming(true)
+
+ var latest: Boolean? = null
+ val job = underTest.roaming.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isTrue()
+
+ repository.setAllRoaming(false)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun dataActivity_nullWhenConfigIsOff() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(false)
+ createAndSetViewModel()
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this)
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+ fun dataActivity_configOn_testIndicators_staticFlagOff() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(true)
+ createAndSetViewModel()
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+
+ yield()
+
+ assertThat(inVisible).isTrue()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isTrue()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isFalse()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS)
+ fun dataActivity_configOn_testIndicators_staticFlagOn() =
+ testScope.runTest {
+ // Create a new view model here so the constants are properly read
+ whenever(constants.shouldShowActivityConfig).thenReturn(true)
+ createAndSetViewModel()
+
+ var inVisible: Boolean? = null
+ val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
+
+ var outVisible: Boolean? = null
+ val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this)
+
+ var containerVisible: Boolean? = null
+ val containerJob =
+ underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this)
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+
+ yield()
+
+ assertThat(inVisible).isTrue()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isTrue()
+ assertThat(containerVisible).isTrue()
+
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+
+ assertThat(inVisible).isFalse()
+ assertThat(outVisible).isFalse()
+ assertThat(containerVisible).isTrue()
+
+ inJob.cancel()
+ outJob.cancel()
+ containerJob.cancel()
+ }
+
+ @Test
+ fun netTypeBackground_nullWhenNoPrioritizedCapabilities() =
+ testScope.runTest {
+ createAndSetViewModel()
+
+ val latest by collectLastValue(underTest.networkTypeBackground)
+
+ repository.hasPrioritizedNetworkCapabilities.value = false
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun netTypeBackground_sliceUiEnabled_notNullWhenPrioritizedCapabilities_newIcons() =
+ testScope.runTest {
+ createAndSetViewModel()
+
+ val latest by collectLastValue(underTest.networkTypeBackground)
+
+ repository.hasPrioritizedNetworkCapabilities.value = true
+
+ assertThat(latest)
+ .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background_updated, null))
+ }
+
+ @Test
+ @DisableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun netTypeBackground_sliceUiDisabled_notNullWhenPrioritizedCapabilities_oldIcons() =
+ testScope.runTest {
+ createAndSetViewModel()
+
+ val latest by collectLastValue(underTest.networkTypeBackground)
+
+ repository.hasPrioritizedNetworkCapabilities.value = true
+
+ assertThat(latest)
+ .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null))
+ }
+
+ @Test
+ fun nonTerrestrial_defaultProperties() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+
+ val roaming by collectLastValue(underTest.roaming)
+ val networkTypeIcon by collectLastValue(underTest.networkTypeIcon)
+ val networkTypeBackground by collectLastValue(underTest.networkTypeBackground)
+ val activityInVisible by collectLastValue(underTest.activityInVisible)
+ val activityOutVisible by collectLastValue(underTest.activityOutVisible)
+ val activityContainerVisible by collectLastValue(underTest.activityContainerVisible)
+
+ assertThat(roaming).isFalse()
+ assertThat(networkTypeIcon).isNull()
+ assertThat(networkTypeBackground).isNull()
+ assertThat(activityInVisible).isFalse()
+ assertThat(activityOutVisible).isFalse()
+ assertThat(activityContainerVisible).isFalse()
+ }
+
+ @Test
+ fun nonTerrestrial_ignoresDefaultProperties() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+
+ val roaming by collectLastValue(underTest.roaming)
+ val networkTypeIcon by collectLastValue(underTest.networkTypeIcon)
+ val networkTypeBackground by collectLastValue(underTest.networkTypeBackground)
+ val activityInVisible by collectLastValue(underTest.activityInVisible)
+ val activityOutVisible by collectLastValue(underTest.activityOutVisible)
+ val activityContainerVisible by collectLastValue(underTest.activityContainerVisible)
+
+ repository.setAllRoaming(true)
+ repository.setNetworkTypeKey(connectionsRepository.LTE_KEY)
+ // sets the background on cellular
+ repository.hasPrioritizedNetworkCapabilities.value = true
+ repository.dataActivityDirection.value =
+ DataActivityModel(hasActivityIn = true, hasActivityOut = true)
+
+ assertThat(roaming).isFalse()
+ assertThat(networkTypeIcon).isNull()
+ assertThat(networkTypeBackground).isNull()
+ assertThat(activityInVisible).isFalse()
+ assertThat(activityOutVisible).isFalse()
+ assertThat(activityContainerVisible).isFalse()
+ }
+
+ @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun nonTerrestrial_usesSatelliteIcon_flagOff() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+ repository.setAllLevels(0)
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.setAllLevels(1)
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.setAllLevels(2)
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.setAllLevels(3)
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.setAllLevels(4)
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun nonTerrestrial_usesSatelliteIcon_flagOn() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun satelliteIcon_ignoresInflateSignalStrength_flagOff() =
+ testScope.runTest {
+ // Note that this is the exact same test as above, but with inflateSignalStrength set to
+ // true we note that the level is unaffected by inflation
+ repository.inflateSignalStrength.value = true
+ repository.isNonTerrestrial.value = true
+ repository.setAllLevels(0)
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.setAllLevels(1)
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.setAllLevels(2)
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.setAllLevels(3)
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.setAllLevels(4)
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Test
+ fun satelliteIcon_ignoresInflateSignalStrength_flagOn() =
+ testScope.runTest {
+ // Note that this is the exact same test as above, but with inflateSignalStrength set to
+ // true we note that the level is unaffected by inflation
+ repository.inflateSignalStrength.value = true
+ repository.isNonTerrestrial.value = true
+ repository.satelliteLevel.value = 0
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.satelliteLevel.value = 1
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.satelliteLevel.value = 2
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.satelliteLevel.value = 3
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.satelliteLevel.value = 4
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
+ private fun createAndSetViewModel() {
+ underTest =
+ MobileIconViewModelKairos(
+ SUB_1_ID,
+ interactor,
+ airplaneModeInteractor,
+ constants,
+ testScope.backgroundScope,
+ )
+ }
+
+ companion object {
+ private const val SUB_1_ID = 1
+
+ // For convenience, just define these as constants
+ private val NO_SIGNAL = R.string.accessibility_no_signal
+ private val ONE_BAR = R.string.accessibility_one_bar
+ private val TWO_BARS = R.string.accessibility_two_bars
+ private val THREE_BARS = R.string.accessibility_three_bars
+ private val FOUR_BARS = R.string.accessibility_four_bars
+ private val FULL_BARS = R.string.accessibility_signal_full
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt
new file mode 100644
index 000000000000..e921430394c2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MobileIconsViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: MobileIconsViewModelKairos
+ private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
+ private val flags = FakeFeatureFlagsClassic()
+
+ private lateinit var airplaneModeInteractor: AirplaneModeInteractor
+ @Mock private lateinit var constants: ConnectivityConstants
+ @Mock private lateinit var logger: MobileViewLogger
+ @Mock private lateinit var verboseLogger: VerboseMobileViewLogger
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ airplaneModeInteractor =
+ AirplaneModeInteractor(
+ FakeAirplaneModeRepository(),
+ FakeConnectivityRepository(),
+ kosmos.fakeMobileConnectionsRepository,
+ )
+
+ underTest =
+ MobileIconsViewModelKairos(
+ logger,
+ verboseLogger,
+ interactor,
+ airplaneModeInteractor,
+ constants,
+ testScope.backgroundScope,
+ )
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ }
+
+ @Test
+ fun subscriptionIdsFlow_matchesInteractor() =
+ testScope.runTest {
+ var latest: List<Int>? = null
+ val job = underTest.subscriptionIdsFlow.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value =
+ listOf(
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ )
+ assertThat(latest).isEqualTo(listOf(1))
+
+ interactor.filteredSubscriptions.value =
+ listOf(
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ SubscriptionModel(
+ subscriptionId = 5,
+ isOpportunistic = true,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ SubscriptionModel(
+ subscriptionId = 7,
+ isOpportunistic = true,
+ carrierName = "Carrier 7",
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ )
+ assertThat(latest).isEqualTo(listOf(2, 5, 7))
+
+ interactor.filteredSubscriptions.value = emptyList()
+ assertThat(latest).isEmpty()
+
+ job.cancel()
+ }
+
+ @Test
+ fun caching_mobileIconViewModelIsReusedForSameSubId() =
+ testScope.runTest {
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS)
+
+ assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl)
+ }
+
+ @Test
+ fun caching_invalidViewModelsAreRemovedFromCacheWhenSubDisappears() =
+ testScope.runTest {
+ // Retrieve models to trigger caching
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS)
+
+ // Both impls are cached
+ assertThat(underTest.reuseCache.keys).containsExactly(1, 2)
+
+ // SUB_1 is removed from the list...
+ interactor.filteredSubscriptions.value = listOf(SUB_2)
+
+ // ... and dropped from the cache
+ assertThat(underTest.reuseCache.keys).containsExactly(2)
+ }
+
+ @Test
+ fun caching_invalidatedViewModelsAreCanceled() =
+ testScope.runTest {
+ // Retrieve models to trigger caching
+ val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME)
+ val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS)
+
+ var scope1 = underTest.reuseCache[1]?.second
+ var scope2 = underTest.reuseCache[2]?.second
+
+ // Scopes are not canceled
+ assertTrue(scope1!!.isActive)
+ assertTrue(scope2!!.isActive)
+
+ // SUB_1 is removed from the list...
+ interactor.filteredSubscriptions.value = listOf(SUB_2)
+
+ // scope1 is canceled
+ assertFalse(scope1!!.isActive)
+ assertTrue(scope2!!.isActive)
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = emptyList()
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_oneSub_notShowingRat_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1)
+ // The unknown icon group doesn't show a RAT
+ interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_oneSub_showingRat_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1)
+ // The 3G icon group will show a RAT
+ interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_updatesAsSubUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1)
+ val sub1Interactor = interactor.getInteractorForSubId(1)!!
+
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+ assertThat(latest).isTrue()
+
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ assertThat(latest).isFalse()
+
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.LTE)
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubNotShowingRat_false() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+ interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubShowingRat_true() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+
+ assertThat(latest).isTrue()
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_subListUpdates_valAlsoUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+
+ assertThat(latest).isTrue()
+
+ // WHEN the sub list gets new subscriptions where the last subscription is not showing
+ // the network type icon
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3)
+ interactor.getInteractorForSubId(3)!!.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+
+ // THEN the flow updates
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
+ fun firstMobileSubShowingNetworkTypeIcon_subListReorders_valAlsoUpdates() =
+ testScope.runTest {
+ var latest: Boolean? = null
+ val job =
+ underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ // Immediately switch the order so that we've created both interactors
+ interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1)
+ val sub1Interactor = interactor.getInteractorForSubId(1)!!
+ val sub2Interactor = interactor.getInteractorForSubId(2)!!
+
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ sub1Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
+ sub2Interactor.networkTypeIconGroup.value =
+ NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
+ assertThat(latest).isTrue()
+
+ // WHEN sub1 becomes last and sub1 has no network type icon
+ interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1)
+
+ // THEN the flow updates
+ assertThat(latest).isFalse()
+
+ // WHEN sub2 becomes last and sub2 has a network type icon
+ interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+
+ // THEN the flow updates
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ companion object {
+ private val SUB_1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_3 =
+ SubscriptionModel(
+ subscriptionId = 3,
+ isOpportunistic = false,
+ carrierName = "Carrier 3",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt
new file mode 100644
index 000000000000..ce35d9d8610f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.platform.test.annotations.EnableFlags
+import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.core.StatusBarRootModernization
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StackedMobileIconViewModelKairosTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val testScope = kosmos.testScope
+
+ private val Kosmos.underTest: StackedMobileIconViewModelKairos by Fixture {
+ stackedMobileIconViewModelKairos
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.underTest.activateIn(testScope)
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun dualSim_filtersOutNonDualConnections() =
+ kosmos.runTest {
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf()
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3)
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ assertThat(underTest.dualSim).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun dualSim_filtersOutNonCellularIcons() =
+ kosmos.runTest {
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
+ assertThat(underTest.dualSim).isNull()
+
+ fakeMobileIconsInteractor
+ .getInteractorForSubId(SUB_1.subscriptionId)!!
+ .signalLevelIcon
+ .value =
+ SignalIconModel.Satellite(
+ level = 0,
+ icon = Icon.Resource(res = 0, contentDescription = null),
+ )
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ assertThat(underTest.dualSim).isNull()
+ }
+
+ @Test
+ @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
+ fun dualSim_tracksActiveSubId() =
+ kosmos.runTest {
+ // Active sub id is null, order is unchanged
+ fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ setIconLevel(SUB_1.subscriptionId, 1)
+ setIconLevel(SUB_2.subscriptionId, 2)
+
+ assertThat(underTest.dualSim!!.primary.level).isEqualTo(1)
+ assertThat(underTest.dualSim!!.secondary.level).isEqualTo(2)
+
+ // Active sub is 2, order is swapped
+ fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId
+
+ assertThat(underTest.dualSim!!.primary.level).isEqualTo(2)
+ assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1)
+ }
+
+ private fun setIconLevel(subId: Int, level: Int) {
+ with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) {
+ signalLevelIcon.value =
+ (signalLevelIcon.value as SignalIconModel.Cellular).copy(level = level)
+ }
+ }
+
+ companion object {
+ private val SUB_1 =
+ SubscriptionModel(
+ subscriptionId = 1,
+ isOpportunistic = false,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_2 =
+ SubscriptionModel(
+ subscriptionId = 2,
+ isOpportunistic = false,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ private val SUB_3 =
+ SubscriptionModel(
+ subscriptionId = 3,
+ isOpportunistic = false,
+ carrierName = "Carrier 3",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt
new file mode 100644
index 000000000000..fb19a884a210
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectInteractorTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
+import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SqueezeEffectInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ SqueezeEffectInteractor(
+ squeezeEffectRepository = fakeSqueezeEffectRepository
+ )
+ }
+
+ @Test
+ fun testIsSqueezeEffectDisabled_whenDisabledInRepository() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @Test
+ fun testIsSqueezeEffectEnabled_whenEnabledInRepository() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt
new file mode 100644
index 000000000000..5d8d3f90e13c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/SqueezeEffectRepositoryTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects
+
+import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shared.Flags
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl
+import com.android.systemui.util.settings.FakeGlobalSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SqueezeEffectRepositoryTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val globalSettings = FakeGlobalSettings(StandardTestDispatcher())
+
+ @Mock
+ private lateinit var bgHandler: Handler
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ SqueezeEffectRepositoryImpl(
+ bgHandler = bgHandler,
+ bgCoroutineContext = testScope.testScheduler,
+ globalSettings = globalSettings
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectDisabled_WhenFlagDisabled() =
+ kosmos.runTest {
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectDisabled_WhenFlagEnabled_GlobalSettingsDisabled() =
+ kosmos.runTest {
+ globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 0)
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectEnabled_WhenFlagEnabled_GlobalSettingEnabled() =
+ kosmos.runTest {
+ globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt
new file mode 100644
index 000000000000..83dc45c8c511
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects
+
+import android.view.View
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCapture
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
+import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
+import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TopLevelWindowEffectsTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ @Mock
+ private lateinit var windowManager: WindowManager
+
+ @Mock
+ private lateinit var viewCapture: Lazy<ViewCapture>
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ TopLevelWindowEffects(
+ context = mContext,
+ applicationScope = testScope.backgroundScope,
+ windowManager = ViewCaptureAwareWindowManager(
+ windowManager = windowManager,
+ lazyViewCapture = viewCapture,
+ isViewCaptureEnabled = false
+ ),
+ squeezeEffectInteractor = SqueezeEffectInteractor(
+ squeezeEffectRepository = fakeSqueezeEffectRepository
+ )
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ doNothing().whenever(windowManager).addView(any<View>(), any<WindowManager.LayoutParams>())
+ doNothing().whenever(windowManager).removeView(any<View>())
+ doNothing().whenever(windowManager).removeView(any<EffectsWindowRoot>())
+ }
+
+ @Test
+ fun noWindowWhenSqueezeEffectDisabled() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false
+
+ underTest.start()
+
+ verify(windowManager, never()).addView(any<View>(), any<WindowManager.LayoutParams>())
+ }
+
+ @Test
+ fun addViewToWindowWhenSqueezeEffectEnabled() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true
+
+ underTest.start()
+
+ verify(windowManager, times(1)).addView(any<View>(),
+ any<WindowManager.LayoutParams>())
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt
index fdfcdc486c02..9a58365116fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt
@@ -16,14 +16,13 @@
package com.android.systemui.util.wakelock
-import android.os.Build
import android.os.PowerManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.assertLogsWtf
import org.junit.After
import org.junit.Assert
-import org.junit.Assume
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -82,12 +81,11 @@ class ClientTrackingWakeLockTest : SysuiTestCase() {
@Test
fun wakeLock_releasedTooManyTimes_stillReleased_noThrow() {
- Assume.assumeFalse(Build.IS_ENG)
mWakeLock.acquire(WHY)
mWakeLock.acquire(WHY_2)
mWakeLock.release(WHY)
mWakeLock.release(WHY_2)
- mWakeLock.release(WHY)
+ assertLogsWtf { mWakeLock.release(WHY) }
Assert.assertFalse(mInner.isHeld)
}
@@ -104,9 +102,8 @@ class ClientTrackingWakeLockTest : SysuiTestCase() {
@Test
fun prodBuild_wakeLock_releaseWithoutAcquire_noThrow() {
- Assume.assumeFalse(Build.IS_ENG)
// shouldn't throw an exception on production builds
- mWakeLock.release(WHY)
+ assertLogsWtf { mWakeLock.release(WHY) }
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java
index 90aecfb62e91..3951670c4125 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java
@@ -19,16 +19,15 @@ package com.android.systemui.util.wakelock;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import android.os.Build;
import android.os.PowerManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.log.LogAssertKt;
import org.junit.After;
-import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -93,8 +92,7 @@ public class WakeLockTest extends SysuiTestCase {
@Test
public void prodBuild_wakeLock_releaseWithoutAcquire_noThrow() {
- Assume.assumeFalse(Build.IS_ENG);
// shouldn't throw an exception on production builds
- mWakeLock.release(WHY);
+ LogAssertKt.assertRunnableLogsWtf(() -> mWakeLock.release(WHY));
}
}
diff --git a/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml
new file mode 100644
index 000000000000..d398f60ddc3c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/notification_ripple_untinted_color">
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="8dp"
+ android:insetRight="0dp"
+ android:insetBottom="8dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/notification_2025_smart_reply_button_corner_radius" />
+ <stroke android:width="@dimen/smart_reply_button_stroke_width"
+ android:color="@color/smart_reply_button_stroke" />
+ <solid android:color="@color/smart_reply_button_background"/>
+ </shape>
+ </inset>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index 6f8b4cd3e4a9..9b629ace76af 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -15,8 +15,8 @@
~ limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/media_output_dialog"
android:layout_width="@dimen/large_dialog_width"
android:layout_height="wrap_content"
@@ -35,24 +35,25 @@
android:orientation="horizontal">
<ImageView
android:id="@+id/header_icon"
- android:layout_width="72dp"
- android:layout_height="72dp"
+ android:layout_width="@dimen/media_output_dialog_header_album_icon_size"
+ android:layout_height="@dimen/media_output_dialog_header_album_icon_size"
+ android:layout_marginEnd="@dimen/media_output_dialog_header_icon_padding"
android:importantForAccessibility="no"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingStart="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:gravity="center_vertical"
+ android:gravity="top"
android:orientation="horizontal">
<ImageView
android:id="@+id/app_source_icon"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:layout_marginBottom="7dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"/>
@@ -103,12 +104,11 @@
android:layout_height="wrap_content" >
</ViewStub>
- <LinearLayout
+ <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/device_list"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical">
+ android:layout_height="0dp"
+ android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_result"
@@ -116,8 +116,11 @@
android:paddingTop="8dp"
android:clipToPadding="false"
android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- </LinearLayout>
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintHeight_max="@dimen/media_output_dialog_list_max_height"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/notif_half_shelf.xml b/packages/SystemUI/res/layout/notif_half_shelf.xml
index 9a66ca9f3baa..603cc00a312c 100644
--- a/packages/SystemUI/res/layout/notif_half_shelf.xml
+++ b/packages/SystemUI/res/layout/notif_half_shelf.xml
@@ -71,15 +71,18 @@
android:textSize="16sp"
/>
- <Switch
- android:id="@+id/toggle"
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ android:id="@+id/material_toggle"
+ android:filterTouchesWhenObscured="false"
+ android:clickable="true"
+ android:focusable="true"
+ android:padding="8dp"
android:layout_height="48dp"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical"
- android:padding="8dp"
- android:track="@drawable/settingslib_track_selector"
- android:thumb="@drawable/settingslib_thumb_selector"
- android:theme="@style/MainSwitch.Settingslib"/>
+ style="@style/SettingslibSwitchStyle.Expressive"/>
+
</com.android.systemui.statusbar.notification.row.AppControlView>
<ScrollView
diff --git a/packages/SystemUI/res/layout/notif_half_shelf_row.xml b/packages/SystemUI/res/layout/notif_half_shelf_row.xml
index b2eaa6ce92b5..4bc37f82a9d7 100644
--- a/packages/SystemUI/res/layout/notif_half_shelf_row.xml
+++ b/packages/SystemUI/res/layout/notif_half_shelf_row.xml
@@ -80,15 +80,16 @@
/>
</RelativeLayout>
- <Switch
- android:id="@+id/toggle"
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ android:id="@+id/material_toggle"
+ android:filterTouchesWhenObscured="false"
+ android:clickable="true"
+ android:focusable="true"
+ android:padding="8dp"
android:layout_height="48dp"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical"
- android:padding="8dp"
- android:track="@drawable/settingslib_track_selector"
- android:thumb="@drawable/settingslib_thumb_selector"
- android:theme="@style/MainSwitch.Settingslib"
- />
+ style="@style/SettingslibSwitchStyle.Expressive"/>
</LinearLayout>
</com.android.systemui.statusbar.notification.row.ChannelRow>
diff --git a/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml
new file mode 100644
index 000000000000..ed905885a76f
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml
@@ -0,0 +1,35 @@
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/notification_2025_smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/notification_2025_smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="google-sans-flex"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:textColor="@color/smart_reply_button_text"
+ android:paddingStart="@dimen/smart_reply_button_action_padding_left"
+ android:paddingEnd="@dimen/smart_reply_button_padding_horizontal"
+ android:drawablePadding="@dimen/smart_action_button_icon_padding"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml
new file mode 100644
index 000000000000..4f543e5099bf
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/notification_2025_smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/notification_2025_smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="google-sans-flex"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:textColor="@color/smart_reply_button_text"
+ android:paddingStart="@dimen/smart_reply_button_padding_horizontal"
+ android:paddingEnd="@dimen/smart_reply_button_padding_horizontal"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 19e05871c035..d0ae307b6919 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -283,7 +283,7 @@
<dimen name="notification_max_height">358dp</dimen>
<!-- Height of a large promoted ongoing notification in the status bar -->
- <dimen name="notification_max_height_for_promoted_ongoing">272dp</dimen>
+ <dimen name="notification_max_height_for_promoted_ongoing">218dp</dimen>
<!-- Height of a heads up notification in the status bar for legacy custom views -->
<dimen name="notification_max_heads_up_height_legacy">128dp</dimen>
@@ -1157,6 +1157,8 @@
<dimen name="smart_action_button_icon_size">18dp</dimen>
<dimen name="smart_action_button_icon_padding">8dp</dimen>
<dimen name="smart_action_button_outline_stroke_width">2dp</dimen>
+ <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen>
+ <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen>
<!-- Magic Action params. -->
<!-- Corner radius = half of min_height to create rounded sides. -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c06c17a0844f..8c1fd65d96d4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2357,8 +2357,8 @@
<string name="system_multitasking_lhs">Use split screen with app on the left</string>
<!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
<string name="system_multitasking_full_screen">Use full screen</string>
- <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] -->
- <string name="system_multitasking_desktop_view">Use desktop view</string>
+ <!-- User visible title for the keyboard shortcut that switches to desktop windowing [CHAR LIMIT=70] -->
+ <string name="system_multitasking_desktop_view">Use desktop windowing</string>
<!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
<string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
<!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 4a8e4ed3f6f1..f72087efc03e 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -32,6 +32,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -42,7 +43,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.LauncherProxyService;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.stack.AmbientState;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 19caead525c9..fb3bc620ee68 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.shared.QSSettingsPackageRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -111,6 +112,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
private final HearingDevicesUiEventLogger mUiEventLogger;
private final boolean mShowPairNewDevice;
private final int mLaunchSourceId;
+ private final QSSettingsPackageRepository mQSSettingsPackageRepository;
private SystemUIDialog mDialog;
private HearingDevicesListAdapter mDeviceListAdapter;
@@ -166,7 +168,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@Main Executor mainExecutor,
@Background Executor bgExecutor,
AudioManager audioManager,
- HearingDevicesUiEventLogger uiEventLogger) {
+ HearingDevicesUiEventLogger uiEventLogger,
+ QSSettingsPackageRepository qsSettingsPackageRepository) {
mShowPairNewDevice = showPairNewDevice;
mSystemUIDialogFactory = systemUIDialogFactory;
mActivityStarter = activityStarter;
@@ -178,6 +181,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
mProfileManager = localBluetoothManager.getProfileManager();
mUiEventLogger = uiEventLogger;
mLaunchSourceId = launchSourceId;
+ mQSSettingsPackageRepository = qsSettingsPackageRepository;
}
@Override
@@ -193,11 +197,11 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, mLaunchSourceId);
dismissDialogIfExists();
- Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS);
Bundle bundle = new Bundle();
bundle.putString(KEY_BLUETOOTH_ADDRESS, deviceItem.getCachedBluetoothDevice().getAddress());
- intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS)
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName())
+ .putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
mDialogTransitionAnimator.createActivityTransitionController(view));
}
@@ -396,8 +400,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
pairButton.setOnClickListener(v -> {
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, mLaunchSourceId);
dismissDialogIfExists();
- final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS)
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName());
mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
mDialogTransitionAnimator.createActivityTransitionController(dialog));
});
@@ -518,8 +522,9 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
com.android.internal.R.color.materialColorOnPrimaryContainer));
}
text.setText(item.getToolName());
- Intent intent = item.getToolIntent();
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Intent intent = item.getToolIntent()
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName());
+
view.setOnClickListener(v -> {
final String name = intent.getComponent() != null
? intent.getComponent().flattenToString()
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
index 52204b84346d..6aeb35b3b158 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
@@ -19,6 +19,7 @@ package com.android.systemui.brightness.ui.compose
import android.content.Context
import android.view.MotionEvent
import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.VectorConverter
@@ -40,6 +41,7 @@ import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -323,7 +325,7 @@ private fun Modifier.sliderBackground(color: Color) = drawWithCache {
fun BrightnessSliderContainer(
viewModel: BrightnessSliderViewModel,
modifier: Modifier = Modifier,
- containerColor: Color = colorResource(R.color.shade_scrim_background_dark),
+ containerColors: ContainerColors,
) {
val gamma = viewModel.currentBrightness.value
if (gamma == BrightnessSliderViewModel.initialValue.value) { // Ignore initial negative value.
@@ -344,6 +346,16 @@ fun BrightnessSliderContainer(
DisposableEffect(Unit) { onDispose { viewModel.setIsDragging(false) } }
+ var dragging by remember { mutableStateOf(false) }
+
+ // Use dragging instead of viewModel.showMirror so the color starts changing as soon as the
+ // dragging state changes. If not, we may be waiting for the background to finish fading in
+ // when stopping dragging
+ val containerColor by
+ animateColorAsState(
+ if (dragging) containerColors.mirrorColor else containerColors.idleColor
+ )
+
Box(
modifier =
modifier
@@ -360,10 +372,12 @@ fun BrightnessSliderContainer(
onRestrictedClick = viewModel::showPolicyRestrictionDialog,
onDrag = {
viewModel.setIsDragging(true)
+ dragging = true
coroutineScope.launch { viewModel.onDrag(Drag.Dragging(GammaBrightness(it))) }
},
onStop = {
viewModel.setIsDragging(false)
+ dragging = false
coroutineScope.launch { viewModel.onDrag(Drag.Stopped(GammaBrightness(it))) }
},
modifier =
@@ -392,6 +406,15 @@ fun BrightnessSliderContainer(
}
}
+data class ContainerColors(val idleColor: Color, val mirrorColor: Color) {
+ companion object {
+ fun singleColor(color: Color) = ContainerColors(color, color)
+
+ val defaultContainerColor: Color
+ @Composable @ReadOnlyComposable get() = colorResource(R.color.shade_panel_fallback)
+ }
+}
+
private object Dimensions {
val SliderBackgroundFrameSize = DpSize(10.dp, 6.dp)
val SliderBackgroundRoundedCorner = 24.dp
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 1ef3a950ba0d..11b42a8eafd6 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -36,7 +36,8 @@ import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.education.dagger.ContextualEducationModule;
-import com.android.systemui.effects.dagger.TopLevelWindowEffectsModule;
+import com.android.systemui.topwindoweffects.dagger.SqueezeEffectRepositoryModule;
+import com.android.systemui.topwindoweffects.dagger.TopLevelWindowEffectsModule;
import com.android.systemui.emergency.EmergencyGestureModule;
import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialModule;
import com.android.systemui.keyboard.shortcut.ShortcutHelperModule;
@@ -161,6 +162,7 @@ import javax.inject.Named;
StatusBarPhoneModule.class,
SystemActionsModule.class,
ShadeModule.class,
+ SqueezeEffectRepositoryModule.class,
StartCentralSurfacesModule.class,
SceneContainerFrameworkModule.class,
SysUICoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
index 083191c8ecde..a56710ee3772 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
@@ -32,9 +32,6 @@ class FakePerDisplayRepository<T> : PerDisplayRepository<T> {
return instances[displayId]
}
- override val displayIds: Set<Int>
- get() = instances.keys
-
override val debugName: String
get() = "FakePerDisplayRepository"
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
index d27e33e53dbb..d1d013542fbf 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
@@ -86,9 +86,6 @@ interface PerDisplayRepository<T> {
/** Gets the cached instance or create a new one for a given display. */
operator fun get(displayId: Int): T?
- /** List of display ids for which this repository has an instance. */
- val displayIds: Set<Int>
-
/** Debug name for this repository, mainly for tracing and logging. */
val debugName: String
}
@@ -122,9 +119,6 @@ constructor(
backgroundApplicationScope.launch("$debugName#start") { start() }
}
- override val displayIds: Set<Int>
- get() = perDisplayInstances.keys
-
private suspend fun start() {
dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this)
displayRepository.displayIds.collectLatest { displayIds ->
@@ -199,8 +193,6 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
private val lazyDefaultDisplayInstance by lazy {
instanceProvider.createInstance(Display.DEFAULT_DISPLAY)
}
- override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY)
-
override fun get(displayId: Int): T? = lazyDefaultDisplayInstance
}
@@ -214,7 +206,5 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
*/
class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) :
PerDisplayRepository<T> {
- override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY)
-
override fun get(displayId: Int): T? = instance
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 3b85b571023f..9ade5036b9a8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -51,10 +51,10 @@ import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.SystemUIAppComponentFactoryBase;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index c4a7e1ed95e1..55fac3c7644b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -36,6 +36,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransit
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
@@ -272,6 +273,12 @@ abstract class DeviceEntryIconTransitionModule {
@Binds
@IntoSet
+ abstract fun glanceableHubToLockscreen(
+ impl: GlanceableHubToLockscreenTransitionViewModel
+ ): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
abstract fun occludedToGlanceableHub(
impl: OccludedToGlanceableHubTransitionViewModel
): DeviceEntryIconTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
index 0ccb24a9858a..20fc88446ce3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt
@@ -61,6 +61,7 @@ constructor(
primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel,
lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
glanceableHubToAodTransitionViewModel: GlanceableHubToAodTransitionViewModel,
+ glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
) {
val color: Flow<Int> =
deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground ->
@@ -108,6 +109,7 @@ constructor(
.deviceEntryBackgroundViewAlpha,
lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha,
glanceableHubToAodTransitionViewModel.deviceEntryBackgroundViewAlpha,
+ glanceableHubToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha,
)
.merge()
.onStart {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index b4b4c82c59b9..bcbe66642d11 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -27,6 +27,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import com.android.systemui.keyguard.ui.StateToValue
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import com.android.systemui.keyguard.ui.transitions.GlanceableHubTransition
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
@@ -49,7 +50,7 @@ constructor(
@ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
animationFlow: KeyguardTransitionAnimationFlow,
private val blurFactory: GlanceableHubBlurComponent.Factory,
-) : GlanceableHubTransition {
+) : GlanceableHubTransition, DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow
.setup(
@@ -102,4 +103,8 @@ constructor(
val notificationTranslationX: Flow<Float> =
keyguardTranslationX.map { it.value }.filterNotNull()
+
+ val deviceEntryBackgroundViewAlpha: Flow<Float> = keyguardAlpha
+
+ override val deviceEntryParentViewAlpha: Flow<Float> = keyguardAlpha
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index d4676bc9c146..6d8a943d3e28 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -132,6 +132,8 @@ constructor(
private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ private val occludedToPrimaryBouncerTransitionViewModel:
+ OccludedToPrimaryBouncerTransitionViewModel,
private val offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel,
private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
@@ -288,6 +290,7 @@ constructor(
occludedToAodTransitionViewModel.lockscreenAlpha,
occludedToDozingTransitionViewModel.lockscreenAlpha,
occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+ occludedToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
offToLockscreenTransitionViewModel.lockscreenAlpha,
primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
index f14a5a282e88..67c3071db390 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
@@ -45,6 +45,12 @@ constructor(
)
.setupWithoutSceneContainer(edge = Edge.create(OCCLUDED, PRIMARY_BOUNCER))
+ /**
+ * Reasserts that lockscreen content should not be visible. It is possible the keyguard alpha is
+ * set to 1f if coming from an expanded shade that collapsed to launch an occluding activity.
+ */
+ val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
override val windowBlurRadius: Flow<Float> =
shadeDependentFlows.transitionFlow(
flowWhenShadeIsExpanded =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java
index 18f4b4ab9b88..db4c7a5b2ee5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/media/NotificationMediaManager.java
@@ -11,9 +11,9 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
-package com.android.systemui.statusbar;
+package com.android.systemui.media;
import static com.android.systemui.Flags.mediaControlsUserInitiatedDeleteintent;
@@ -40,6 +40,8 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
import com.android.systemui.media.controls.shared.model.MediaData;
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData;
+import com.android.systemui.statusbar.NotificationPresenter;
+import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 46cf0a63e93d..309b6751176c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -66,6 +66,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
import com.android.systemui.media.controls.domain.resume.MediaResumeListener
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
@@ -83,7 +84,6 @@ import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index 5fef81f4596a..da462e6713c6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -30,6 +30,8 @@ import android.service.notification.StatusBarNotification
import android.util.Log
import androidx.media.utils.MediaConstants
import com.android.systemui.Flags
+import com.android.systemui.media.NotificationMediaManager.isConnectingState
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS
import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS
import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -38,8 +40,6 @@ import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaNotificationAction
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.util.kotlin.logI
private const val TAG = "MediaActions"
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index 8bb7303a8386..dbd2250a75b0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -51,6 +51,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.graphics.ImageLoader
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.shared.model.MediaAction
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
@@ -60,7 +61,6 @@ import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.res.R
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.kotlin.logD
import java.util.concurrent.ConcurrentHashMap
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index fe852ce7979f..94df4b353c94 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -66,6 +66,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.data.repository.MediaDataRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
@@ -85,7 +86,6 @@ import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 684a560b0502..be4e6cc59f76 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -25,12 +25,12 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.media.NotificationMediaManager.isPlayingState
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
index a1f0cc33c065..8744c5c9a838 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
@@ -39,8 +39,8 @@ import androidx.lifecycle.MutableLiveData
import com.android.systemui.Flags
import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.util.concurrency.RepeatableExecutor
import java.util.Locale
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
index c58ba377fb68..ac1672db9375 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -351,8 +351,9 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
@VisibleForTesting
void showCustomEndSessionDialog(MediaDevice device) {
MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
- mContext, () -> transferOutput(device), mController.getColorButtonBackground(),
- mController.getColorItemContent());
+ mContext, () -> transferOutput(device),
+ mController.getColorSchemeLegacy().getColorButtonBackground(),
+ mController.getColorSchemeLegacy().getColorItemContent());
mediaSessionReleaseDialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index 300a3578bb8f..795e811db2bc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -50,9 +50,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.media.flags.Flags;
import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.utils.ThreadUtils;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.res.R;
+import java.util.concurrent.Executor;
/**
* A RecyclerView adapter for the legacy UI media output dialog device list.
*/
@@ -61,13 +63,21 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int UNMUTE_DEFAULT_VOLUME = 2;
- private static final float DEVICE_DISABLED_ALPHA = 0.5f;
- private static final float DEVICE_ACTIVE_ALPHA = 1f;
+ @VisibleForTesting static final float DEVICE_DISABLED_ALPHA = 0.5f;
+ @VisibleForTesting static final float DEVICE_ACTIVE_ALPHA = 1f;
+ private final Executor mMainExecutor;
+ private final Executor mBackgroundExecutor;
View mHolderView;
private boolean mIsInitVolumeFirstTime;
- public MediaOutputAdapterLegacy(MediaSwitchingController controller) {
+ public MediaOutputAdapterLegacy(
+ MediaSwitchingController controller,
+ @Main Executor mainExecutor,
+ @Background Executor backgroundExecutor
+ ) {
super(controller);
+ mMainExecutor = mainExecutor;
+ mBackgroundExecutor = backgroundExecutor;
mIsInitVolumeFirstTime = true;
}
@@ -181,9 +191,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mEndTouchArea.setVisibility(View.GONE);
mEndClickIcon.setVisibility(View.GONE);
mContainerLayout.setOnClickListener(null);
- mTitleText.setTextColor(mController.getColorItemContent());
- mSubTitleText.setTextColor(mController.getColorItemContent());
- mVolumeValueText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
+ mSubTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
+ mVolumeValueText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mIconAreaLayout.setBackground(null);
updateIconAreaClickListener(null);
updateSeekBarProgressColor();
@@ -193,14 +203,14 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
/** Binds a ViewHolder for a "Connect a device" item. */
void onBindPairNewDevice() {
- mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mCheckBox.setVisibility(View.GONE);
updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
updateItemBackground(ConnectionState.DISCONNECTED);
final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
mTitleIcon.setImageDrawable(addDrawable);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
@@ -297,8 +307,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
protected void updateLoadingIndicator(ConnectionState connectionState) {
if (connectionState == ConnectionState.CONNECTING) {
mProgressBar.setVisibility(View.VISIBLE);
- mProgressBar.getIndeterminateDrawable().setTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mProgressBar.getIndeterminateDrawable().setTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
} else {
mProgressBar.setVisibility(View.GONE);
}
@@ -318,8 +328,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
// Connected or connecting state has a darker background.
int backgroundColor = isConnected || isConnecting
- ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground();
+ ? mController.getColorSchemeLegacy().getColorConnectedItemBackground()
+ : mController.getColorSchemeLegacy().getColorItemBackground();
mItemLayout.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
}
@@ -332,13 +342,13 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
private void updateSeekBarProgressColor() {
- mSeekBar.setProgressTintList(
- ColorStateList.valueOf(mController.getColorSeekbarProgress()));
+ mSeekBar.setProgressTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorSeekbarProgress()));
final Drawable contrastDotDrawable =
((LayerDrawable) mSeekBar.getProgressDrawable()).findDrawableByLayerId(
R.id.contrast_dot);
- contrastDotDrawable.setTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ contrastDotDrawable.setTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
}
void updateSeekbarProgressBackground() {
@@ -503,9 +513,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
boolean isInputMediaDevice = device instanceof InputMediaDevice;
int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon);
mTitleIcon.setImageDrawable(mContext.getDrawable(id));
- mTitleIcon.setImageTintList(ColorStateList.valueOf(mController.getColorItemContent()));
- mIconAreaLayout.setBackgroundTintList(
- ColorStateList.valueOf(mController.getColorSeekbarProgress()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
+ mIconAreaLayout.setBackgroundTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorSeekbarProgress()));
}
@VisibleForTesting
@@ -534,8 +545,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mStatusIcon.setVisibility(View.GONE);
} else {
mStatusIcon.setImageDrawable(deviceStatusIcon);
- mStatusIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mStatusIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) deviceStatusIcon).start();
}
@@ -585,9 +596,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private void updateEndAreaWithIcon(View.OnClickListener clickListener,
@DrawableRes int iconDrawableId,
@StringRes int accessibilityStringId) {
- updateEndAreaColor(mController.getColorSeekbarProgress());
+ updateEndAreaColor(mController.getColorSchemeLegacy().getColorSeekbarProgress());
mEndClickIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
mEndClickIcon.setOnClickListener(clickListener);
Drawable drawable = mContext.getDrawable(iconDrawableId);
mEndClickIcon.setImageDrawable(drawable);
@@ -600,8 +612,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
@NonNull GroupStatus groupStatus) {
boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
- updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
- : mController.getColorItemBackground());
+ updateEndAreaColor(groupStatus.selected()
+ ? mController.getColorSchemeLegacy().getColorSeekbarProgress()
+ : mController.getColorSchemeLegacy().getColorItemBackground());
mCheckBox.setContentDescription(mContext.getString(
groupStatus.selected() ? R.string.accessibility_remove_device_from_group
: R.string.accessibility_add_device_to_group));
@@ -611,7 +624,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
!groupStatus.selected(), device) : null);
mCheckBox.setEnabled(isEnabled);
- setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+ setCheckBoxColor(mCheckBox, mController.getColorSchemeLegacy().getColorItemContent());
}
private void setCheckBoxColor(CheckBox checkBox, int color) {
@@ -714,15 +727,15 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
protected void setUpDeviceIcon(@NonNull MediaDevice device) {
- ThreadUtils.postOnBackgroundThread(() -> {
+ mBackgroundExecutor.execute(() -> {
Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext);
- ThreadUtils.postOnMainThread(() -> {
+ mMainExecutor.execute(() -> {
if (!TextUtils.equals(mDeviceId, device.getId())) {
return;
}
mTitleIcon.setImageIcon(icon);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
});
});
}
@@ -807,7 +820,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
void onBind(String groupDividerTitle) {
- mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mTitleText.setText(groupDividerTitle);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index d791361d555f..49d09cf64c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -40,7 +40,6 @@ import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -93,13 +92,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private ImageView mAppResourceIcon;
private ImageView mBroadcastIcon;
private RecyclerView mDevicesRecyclerView;
- private LinearLayout mDeviceListLayout;
+ private ViewGroup mDeviceListLayout;
private LinearLayout mMediaMetadataSectionLayout;
private Button mDoneButton;
private Button mStopButton;
- private int mListMaxHeight;
- private int mItemHeight;
- private int mListPaddingTop;
private WallpaperColors mWallpaperColors;
private boolean mShouldLaunchLeBroadcastDialog;
private boolean mIsLeBroadcastCallbackRegistered;
@@ -109,17 +105,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
protected Executor mExecutor;
- private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
- ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
- int totalItemsHeight = mAdapter.getItemCount() * mItemHeight
- + mListPaddingTop;
- int correctHeight = Math.min(totalItemsHeight, mListMaxHeight);
- // Set max height for list
- if (correctHeight != params.height) {
- params.height = correctHeight;
- mDeviceListLayout.setLayoutParams(params);
- }
- };
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@@ -220,12 +205,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mBroadcastSender = broadcastSender;
mMediaSwitchingController = mediaSwitchingController;
mLayoutManager = new LayoutManagerWrapper(mContext);
- mListMaxHeight = context.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_max_height);
- mItemHeight = context.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_item_height);
- mListPaddingTop = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_padding_top);
mExecutor = Executors.newSingleThreadExecutor();
mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata;
}
@@ -258,8 +237,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon = mDialogView.requireViewById(R.id.app_source_icon);
mBroadcastIcon = mDialogView.requireViewById(R.id.broadcast_icon);
- mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
- mDeviceListLayoutListener);
// Init device list
mLayoutManager.setAutoMeasureEnabled(true);
mDevicesRecyclerView.setLayoutManager(mLayoutManager);
@@ -342,7 +319,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap());
colorSetUpdated = !wallpaperColors.equals(mWallpaperColors);
if (colorSetUpdated) {
- mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn);
+ mMediaSwitchingController.updateCurrentColorScheme(wallpaperColors,
+ isDarkThemeOn);
updateButtonBackgroundColorFilter();
updateDialogBackgroundColor();
}
@@ -359,7 +337,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon.setVisibility(View.GONE);
} else if (appSourceIcon != null) {
Icon appIcon = appSourceIcon.toIcon(mContext);
- mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent());
+ mAppResourceIcon.setColorFilter(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorItemContent());
mAppResourceIcon.setImageIcon(appIcon);
} else {
Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage();
@@ -369,12 +348,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon.setVisibility(View.GONE);
}
}
- if (mHeaderIcon.getVisibility() == View.VISIBLE) {
- final int size = getHeaderIconSize();
- final int padding = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_icon_padding);
- mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
- }
if (!mIncludePlaybackAndAppMetadata) {
mHeaderTitle.setVisibility(View.GONE);
@@ -419,18 +392,19 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private void updateButtonBackgroundColorFilter() {
ColorFilter buttonColorFilter =
new PorterDuffColorFilter(
- mMediaSwitchingController.getColorButtonBackground(),
+ mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
PorterDuff.Mode.SRC_IN);
mDoneButton.getBackground().setColorFilter(buttonColorFilter);
mStopButton.getBackground().setColorFilter(buttonColorFilter);
- mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText());
+ mDoneButton.setTextColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
}
private void updateDialogBackgroundColor() {
- getDialogView()
- .getBackground()
- .setTint(mMediaSwitchingController.getColorDialogBackground());
- mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground());
+ getDialogView().getBackground().setTint(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
+ mDeviceListLayout.setBackgroundColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
}
public void handleLeBroadcastStarted() {
@@ -520,8 +494,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
abstract IconCompat getHeaderIcon();
- abstract int getHeaderIconSize();
-
abstract CharSequence getHeaderText();
abstract CharSequence getHeaderSubtitle();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 9ade9e275ca1..791a61cc73ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -52,6 +52,8 @@ import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.google.zxing.WriterException;
+import java.util.concurrent.Executor;
+
/**
* Dialog for media output broadcast.
*/
@@ -239,13 +241,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
Context context,
boolean aboveStatusbar,
BroadcastSender broadcastSender,
- MediaSwitchingController mediaSwitchingController) {
+ MediaSwitchingController mediaSwitchingController,
+ Executor mainExecutor,
+ Executor backgroundExecutor) {
super(
context,
broadcastSender,
mediaSwitchingController, /* includePlaybackAndAppMetadata */
true);
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
// TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
// that extends MediaOutputBaseDialog
if (!aboveStatusbar) {
@@ -295,12 +300,6 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
}
@Override
- int getHeaderIconSize() {
- return mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_album_icon_size);
- }
-
- @Override
CharSequence getHeaderText() {
return mMediaSwitchingController.getHeaderTitle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
index 2e7e66f5b384..81c85a6ad22d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -20,6 +20,9 @@ import android.content.Context
import android.view.View
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
import javax.inject.Inject
/** Manager to create and show a [MediaOutputBroadcastDialog]. */
@@ -29,7 +32,9 @@ constructor(
private val context: Context,
private val broadcastSender: BroadcastSender,
private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory
+ private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
+ @Main private val mainExecutor: Executor,
+ @Background private val backgroundExecutor: Executor,
) {
var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
@@ -47,7 +52,14 @@ constructor(
/* token */ null,
)
val dialog =
- MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
+ MediaOutputBroadcastDialog(
+ context,
+ aboveStatusBar,
+ broadcastSender,
+ controller,
+ mainExecutor,
+ backgroundExecutor,
+ )
mediaOutputBroadcastDialog = dialog
// Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt
new file mode 100644
index 000000000000..7f0fa463811b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog
+
+import android.content.Context
+import com.android.settingslib.Utils
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.res.R
+
+abstract class MediaOutputColorSchemeLegacy {
+ companion object Factory {
+ @JvmStatic
+ fun fromSystemColors(context: Context): MediaOutputColorSchemeLegacy {
+ return MediaOutputColorSchemeLegacySystem(context)
+ }
+
+ @JvmStatic
+ fun fromDynamicColors(
+ colorScheme: ColorScheme,
+ isDarkTheme: Boolean,
+ ): MediaOutputColorSchemeLegacy {
+ return MediaOutputColorSchemeLegacyDynamic(colorScheme, isDarkTheme)
+ }
+ }
+
+ abstract fun getColorConnectedItemBackground(): Int
+
+ abstract fun getColorPositiveButtonText(): Int
+
+ abstract fun getColorDialogBackground(): Int
+
+ abstract fun getColorItemContent(): Int
+
+ abstract fun getColorSeekbarProgress(): Int
+
+ abstract fun getColorButtonBackground(): Int
+
+ abstract fun getColorItemBackground(): Int
+}
+
+class MediaOutputColorSchemeLegacySystem(private val mContext: Context) :
+ MediaOutputColorSchemeLegacy() {
+
+ override fun getColorConnectedItemBackground() =
+ Utils.getColorStateListDefaultColor(
+ mContext,
+ R.color.media_dialog_connected_item_background,
+ )
+
+ override fun getColorPositiveButtonText() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_solid_button_text)
+
+ override fun getColorDialogBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_background)
+
+ override fun getColorItemContent() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_main_content)
+
+ override fun getColorSeekbarProgress() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_seekbar_progress)
+
+ override fun getColorButtonBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_button_background)
+
+ override fun getColorItemBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_background)
+}
+
+class MediaOutputColorSchemeLegacyDynamic(colorScheme: ColorScheme, isDarkTheme: Boolean) :
+ MediaOutputColorSchemeLegacy() {
+ private var mColorItemContent: Int
+ private var mColorSeekbarProgress: Int
+ private var mColorButtonBackground: Int
+ private var mColorItemBackground: Int
+ private var mColorConnectedItemBackground: Int
+ private var mColorPositiveButtonText: Int
+ private var mColorDialogBackground: Int
+
+ init {
+ if (isDarkTheme) {
+ mColorItemContent = colorScheme.accent1.s100 // A1-100
+ mColorSeekbarProgress = colorScheme.accent2.s600 // A2-600
+ mColorButtonBackground = colorScheme.accent1.s300 // A1-300
+ mColorItemBackground = colorScheme.neutral2.s800 // N2-800
+ mColorConnectedItemBackground = colorScheme.accent2.s800 // A2-800
+ mColorPositiveButtonText = colorScheme.accent2.s800 // A2-800
+ mColorDialogBackground = colorScheme.neutral1.s900 // N1-900
+ } else {
+ mColorItemContent = colorScheme.accent1.s800 // A1-800
+ mColorSeekbarProgress = colorScheme.accent1.s300 // A1-300
+ mColorButtonBackground = colorScheme.accent1.s600 // A1-600
+ mColorItemBackground = colorScheme.accent2.s50 // A2-50
+ mColorConnectedItemBackground = colorScheme.accent1.s100 // A1-100
+ mColorPositiveButtonText = colorScheme.neutral1.s50 // N1-50
+ mColorDialogBackground = colorScheme.backgroundColor
+ }
+ }
+
+ override fun getColorConnectedItemBackground() = mColorConnectedItemBackground
+
+ override fun getColorPositiveButtonText() = mColorPositiveButtonText
+
+ override fun getColorDialogBackground() = mColorDialogBackground
+
+ override fun getColorItemContent() = mColorItemContent
+
+ override fun getColorSeekbarProgress() = mColorSeekbarProgress
+
+ override fun getColorButtonBackground() = mColorButtonBackground
+
+ override fun getColorItemBackground() = mColorItemBackground
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 2e602be4556e..163ff248b9df 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -34,6 +34,8 @@ import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.res.R;
+import java.util.concurrent.Executor;
+
/**
* Dialog for media output transferring.
*/
@@ -49,11 +51,14 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
MediaSwitchingController mediaSwitchingController,
DialogTransitionAnimator dialogTransitionAnimator,
UiEventLogger uiEventLogger,
+ Executor mainExecutor,
+ Executor backgroundExecutor,
boolean includePlaybackAndAppMetadata) {
super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
@@ -76,12 +81,6 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
}
@Override
- int getHeaderIconSize() {
- return mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_album_icon_size);
- }
-
- @Override
CharSequence getHeaderText() {
return mMediaSwitchingController.getHeaderTitle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 4e9451a838ad..d3a81a53b6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -25,6 +25,9 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
import javax.inject.Inject
/** Manager to create and show a [MediaOutputDialog]. */
@@ -37,6 +40,9 @@ constructor(
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
) {
+ @Inject @Main lateinit var mainExecutor: Executor
+ @Inject @Background lateinit var backgroundExecutor: Executor
+
companion object {
const val INTERACTION_JANK_TAG = "media_output"
var mediaOutputDialog: MediaOutputDialog? = null
@@ -51,7 +57,7 @@ constructor(
aboveStatusBar: Boolean,
view: View? = null,
userHandle: UserHandle? = null,
- token: MediaSession.Token? = null
+ token: MediaSession.Token? = null,
) {
createAndShowWithController(
packageName,
@@ -62,8 +68,8 @@ constructor(
it,
DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
- )
+ INTERACTION_JANK_TAG,
+ ),
)
},
userHandle = userHandle,
@@ -128,15 +134,14 @@ constructor(
controller,
dialogTransitionAnimator,
uiEventLogger,
- includePlaybackAndAppMetadata
+ mainExecutor,
+ backgroundExecutor,
+ includePlaybackAndAppMetadata,
)
// Show the dialog.
if (dialogTransitionAnimatorController != null) {
- dialogTransitionAnimator.show(
- mediaOutputDialog,
- dialogTransitionAnimatorController,
- )
+ dialogTransitionAnimator.show(mediaOutputDialog, dialogTransitionAnimatorController)
} else {
mediaOutputDialog.show()
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 02dce406bbee..9e6fa48d6f98 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -83,6 +83,8 @@ import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.dialog.MediaItem.MediaItemType;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -114,6 +116,8 @@ import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;
+import javax.inject.Inject;
+
/**
* Controller for a dialog that allows users to switch media output and input devices, control
* volume, connect to new devices, etc.
@@ -149,7 +153,8 @@ public class MediaSwitchingController
private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
private final MediaSession.Token mToken;
-
+ @Inject @Main Executor mMainExecutor;
+ @Inject @Background Executor mBackgroundExecutor;
@VisibleForTesting
boolean mIsRefreshing = false;
@VisibleForTesting
@@ -163,17 +168,10 @@ public class MediaSwitchingController
@VisibleForTesting
MediaOutputMetricLogger mMetricLogger;
private int mCurrentState;
-
- private int mColorItemContent;
- private int mColorSeekbarProgress;
- private int mColorButtonBackground;
- private int mColorItemBackground;
- private int mColorConnectedItemBackground;
- private int mColorPositiveButtonText;
- private int mColorDialogBackground;
private FeatureFlags mFeatureFlags;
private UserTracker mUserTracker;
private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor;
+ @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -230,20 +228,7 @@ public class MediaSwitchingController
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
- mColorItemContent = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_main_content);
- mColorSeekbarProgress = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_seekbar_progress);
- mColorButtonBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_button_background);
- mColorItemBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_background);
- mColorConnectedItemBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_connected_item_background);
- mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_solid_button_text);
- mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_background);
+ mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
if (enableInputRouting()) {
mInputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -568,26 +553,15 @@ public class MediaSwitchingController
return null;
}
- void setCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
- ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors,
+ void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
+ ColorScheme currentColorScheme = new ColorScheme(wallpaperColors,
isDarkTheme);
- if (isDarkTheme) {
- mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100
- mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600
- mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300
- mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800
- mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800
- mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900
- } else {
- mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800
- mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300
- mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600
- mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100
- mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50
- mColorDialogBackground = mCurrentColorScheme.getBackgroundColor();
- }
+ mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors(
+ currentColorScheme, isDarkTheme);
+ }
+
+ MediaOutputColorSchemeLegacy getColorSchemeLegacy() {
+ return mMediaOutputColorSchemeLegacy;
}
public void refreshDataSetIfNeeded() {
@@ -598,34 +572,6 @@ public class MediaSwitchingController
}
}
- public int getColorConnectedItemBackground() {
- return mColorConnectedItemBackground;
- }
-
- public int getColorPositiveButtonText() {
- return mColorPositiveButtonText;
- }
-
- public int getColorDialogBackground() {
- return mColorDialogBackground;
- }
-
- public int getColorItemContent() {
- return mColorItemContent;
- }
-
- public int getColorSeekbarProgress() {
- return mColorSeekbarProgress;
- }
-
- public int getColorButtonBackground() {
- return mColorButtonBackground;
- }
-
- public int getColorItemBackground() {
- return mColorItemBackground;
- }
-
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
@@ -1097,7 +1043,7 @@ public class MediaSwitchingController
mVolumePanelGlobalStateInteractor,
mUserTracker);
MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
- broadcastSender, controller);
+ broadcastSender, controller, mMainExecutor, mBackgroundExecutor);
dialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt
new file mode 100644
index 000000000000..afed141644ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.interactor
+
+import com.android.systemui.media.remedia.domain.model.MediaSessionModel
+
+/**
+ * Defines interface for classes that can provide business logic in the domain of the media controls
+ * element.
+ */
+interface MediaInteractor {
+
+ /** The list of sessions. Needs to be backed by a compose snapshot state. */
+ val sessions: List<MediaSessionModel>
+
+ /** Seek to [to], in milliseconds on the media session with the given [sessionKey]. */
+ fun seek(sessionKey: Any, to: Long)
+
+ /** Hide the representation of the media session with the given [sessionKey]. */
+ fun hide(sessionKey: Any)
+
+ /** Open media settings. */
+ fun openMediaSettings()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt
new file mode 100644
index 000000000000..02e4d7a966c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaActionModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.model
+
+import com.android.systemui.common.shared.model.Icon
+
+sealed interface MediaActionModel {
+ data class Action(val icon: Icon, val onClick: (() -> Unit)?) : MediaActionModel
+
+ data object ReserveSpace : MediaActionModel
+
+ data object None : MediaActionModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt
new file mode 100644
index 000000000000..d581ae3e4e40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaOutputDeviceModel.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.model
+
+import com.android.systemui.common.shared.model.Icon
+
+data class MediaOutputDeviceModel(val name: String, val icon: Icon, val isInProgress: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt
new file mode 100644
index 000000000000..e64ce73226f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.domain.model
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.graphics.ImageBitmap
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
+import com.android.systemui.media.remedia.shared.model.MediaSessionState
+
+/** Data model representing a media session. */
+@Stable
+interface MediaSessionModel {
+ /** Unique identifier. */
+ val key: Any
+
+ val appName: String
+
+ val appIcon: Icon
+
+ val background: ImageBitmap?
+
+ val colorScheme: MediaColorScheme
+
+ val title: String
+
+ val subtitle: String
+
+ val onClick: () -> Unit
+
+ /**
+ * Whether the session is currently active. Under some UIs, only currently active session should
+ * be shown.
+ */
+ val isActive: Boolean
+
+ /** Whether the session can be hidden/dismissed by the user. */
+ val canBeHidden: Boolean
+
+ /**
+ * Whether the session currently supports scrubbing (e.g. moving to a different position iin the
+ * playback.
+ */
+ val canBeScrubbed: Boolean
+
+ val state: MediaSessionState
+
+ /** The position of the playback within the current track. */
+ val positionMs: Long
+
+ /** The total duration of the current track. */
+ val durationMs: Long
+
+ val outputDevice: MediaOutputDeviceModel
+
+ /** How to lay out the action buttons. */
+ val actionButtonLayout: MediaCardActionButtonLayout
+ val playPauseAction: MediaActionModel
+ val leftAction: MediaActionModel
+ val rightAction: MediaActionModel
+ val additionalActions: List<MediaActionModel.Action>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt
index 91530078e378..c3ce5035accc 100644
--- a/packages/SystemUI/src/com/android/systemui/effects/TopLevelWindowEffects.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaActionState.kt
@@ -14,15 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.effects;
+package com.android.systemui.media.remedia.shared.model
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton;
-import javax.inject.Inject
-
-@SysUISingleton
-class TopLevelWindowEffects @Inject constructor() : CoreStartable {
- override fun start() {
-
- }
+enum class MediaActionState {
+ Enabled,
+ Disabled,
+ Hidden,
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt
new file mode 100644
index 000000000000..554fb1f2fc43
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaCardActionButtonLayout.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.shared.model
+
+enum class MediaCardActionButtonLayout {
+ /** Shows the play/pause button and left/right buttons in privileged positions on the card */
+ WithPlayPause,
+ /** Shows all action buttons along the bottom row. */
+ SecondaryActionsOnly,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt
new file mode 100644
index 000000000000..8dba170f0928
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaColorScheme.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.shared.model
+
+import androidx.compose.ui.graphics.Color
+
+data class MediaColorScheme(val primary: Color, val onPrimary: Color)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
new file mode 100644
index 000000000000..fea5b3267562
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/DismissibleHorizontalPager.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.ui.compose
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerScope
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.thenIf
+import kotlinx.coroutines.launch
+
+/** State for a [DismissibleHorizontalPager] */
+class DismissibleHorizontalPagerState(
+ val isDismissible: Boolean,
+ val isScrollingEnabled: Boolean,
+ val pagerState: PagerState,
+ val offset: Animatable<Float, AnimationVector1D>,
+)
+
+/**
+ * Returns a remembered [DismissibleHorizontalPagerState] that starts at [initialPage] and has
+ * [pageCount] total pages.
+ */
+@Composable
+fun rememberDismissibleHorizontalPagerState(
+ isDismissible: Boolean = true,
+ isScrollingEnabled: Boolean = true,
+ initialPage: Int = 0,
+ pageCount: () -> Int,
+): DismissibleHorizontalPagerState {
+ val pagerState = rememberPagerState(initialPage = initialPage, pageCount = pageCount)
+ val offset = remember { Animatable(0f) }
+
+ return remember(isDismissible, isScrollingEnabled, pagerState, offset) {
+ DismissibleHorizontalPagerState(
+ isDismissible = isDismissible,
+ isScrollingEnabled = isScrollingEnabled,
+ pagerState = pagerState,
+ offset = offset,
+ )
+ }
+}
+
+/**
+ * A [HorizontalPager] that can be swiped-away to dismiss by the user when swiped farther left or
+ * right once fully scrolled to the left-most or right-most page, respectively.
+ */
+@Composable
+fun DismissibleHorizontalPager(
+ state: DismissibleHorizontalPagerState,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+ key: ((Int) -> Any)? = null,
+ pageSpacing: Dp = 0.dp,
+ indicator: @Composable BoxScope.() -> Unit,
+ pageContent: @Composable PagerScope.(page: Int) -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+
+ val nestedScrollConnection = remember {
+ object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ return if (state.offset.value > 0f && available.x < 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else if (state.offset.value < 0f && available.x > 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource,
+ ): Offset {
+ return if (available.x > 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else if (available.x < 0f) {
+ scope.launch { state.offset.snapTo(state.offset.value + available.x) }
+ Offset(available.x, 0f)
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ scope.launch {
+ state.offset.animateTo(
+ if (state.offset.value >= state.pagerState.layoutInfo.pageSize / 2f) {
+ state.pagerState.layoutInfo.pageSize * 2f
+ } else if (
+ state.offset.value <= -state.pagerState.layoutInfo.pageSize / 2f
+ ) {
+ -state.pagerState.layoutInfo.pageSize * 2f
+ } else {
+ 0f
+ }
+ )
+ if (state.offset.value != 0f) {
+ onDismissed()
+ }
+ }
+ return super.onPostFling(consumed, available)
+ }
+ }
+ }
+
+ Box(modifier = modifier) {
+ HorizontalPager(
+ state = state.pagerState,
+ userScrollEnabled = state.isScrollingEnabled,
+ key = key,
+ pageSpacing = pageSpacing,
+ pageContent = pageContent,
+ modifier =
+ Modifier.thenIf(state.isDismissible) {
+ Modifier.nestedScroll(nestedScrollConnection).graphicsLayer {
+ translationX = state.offset.value
+ }
+ },
+ )
+
+ indicator()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
index c9fb8e877009..9c6568057d6f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
@@ -20,6 +20,7 @@ package com.android.systemui.media.remedia.ui.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
@@ -46,6 +47,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -65,12 +67,11 @@ import androidx.compose.material3.SliderState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -90,6 +91,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
@@ -105,20 +107,137 @@ import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayout
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.animation.scene.transitions
-import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.PagerDots
import com.android.systemui.common.ui.compose.load
import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
import com.android.systemui.media.remedia.shared.model.MediaSessionState
import com.android.systemui.media.remedia.ui.viewmodel.MediaCardGutsViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaCardViewModel
+import com.android.systemui.media.remedia.ui.viewmodel.MediaCarouselVisibility
+import com.android.systemui.media.remedia.ui.viewmodel.MediaNavigationViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaOutputSwitcherChipViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaPlayPauseActionViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaSecondaryActionViewModel
-import com.android.systemui.media.remedia.ui.viewmodel.MediaSeekBarViewModel
+import com.android.systemui.media.remedia.ui.viewmodel.MediaViewModel
import kotlin.math.max
+/**
+ * Renders a media controls UI element.
+ *
+ * This composable supports a multitude of presentation styles/layouts controlled by the
+ * [presentationStyle] parameter. If the card carousel can be swiped away to dismiss by the user,
+ * the [onDismissed] callback will be invoked when/if that happens.
+ */
+@Composable
+fun Media(
+ viewModelFactory: MediaViewModel.Factory,
+ presentationStyle: MediaPresentationStyle,
+ behavior: MediaUiBehavior,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val viewModel: MediaViewModel =
+ rememberViewModel("Media.viewModel") {
+ viewModelFactory.create(
+ context = context,
+ carouselVisibility = behavior.carouselVisibility,
+ )
+ }
+
+ CardCarousel(
+ viewModel = viewModel,
+ presentationStyle = presentationStyle,
+ behavior = behavior,
+ onDismissed = onDismissed,
+ modifier = modifier,
+ )
+}
+
+/**
+ * Renders a media controls carousel of cards.
+ *
+ * This composable supports a multitude of presentation styles/layouts controlled by the
+ * [presentationStyle] parameter. The behavior is controlled by [behavior]. If
+ * [MediaUiBehavior.isCarouselDismissible] is `true`, the [onDismissed] callback will be invoked
+ * when/if that happens.
+ */
+@Composable
+private fun CardCarousel(
+ viewModel: MediaViewModel,
+ presentationStyle: MediaPresentationStyle,
+ behavior: MediaUiBehavior,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AnimatedVisibility(visible = viewModel.isCarouselVisible, modifier = modifier) {
+ CardCarouselContent(
+ viewModel = viewModel,
+ presentationStyle = presentationStyle,
+ behavior = behavior,
+ onDismissed = onDismissed,
+ )
+ }
+}
+
+@Composable
+private fun CardCarouselContent(
+ viewModel: MediaViewModel,
+ presentationStyle: MediaPresentationStyle,
+ behavior: MediaUiBehavior,
+ onDismissed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val pagerState =
+ rememberDismissibleHorizontalPagerState(
+ isDismissible = behavior.isCarouselDismissible,
+ isScrollingEnabled = behavior.isCarouselScrollingEnabled,
+ ) {
+ viewModel.cards.size
+ }
+
+ val roundedCornerShape = RoundedCornerShape(32.dp)
+
+ LaunchedEffect(pagerState.pagerState.currentPage) {
+ viewModel.onCardSelected(pagerState.pagerState.currentPage)
+ }
+
+ DismissibleHorizontalPager(
+ state = pagerState,
+ onDismissed = onDismissed,
+ pageSpacing = 8.dp,
+ key = { index -> viewModel.cards[index].key },
+ indicator = {
+ if (pagerState.pagerState.pageCount > 1) {
+ PagerDots(
+ pagerState = pagerState.pagerState,
+ activeColor = Color(0xffdee0ff),
+ nonActiveColor = Color(0xffa7a9ca),
+ dotSize = 6.dp,
+ spaceSize = 6.dp,
+ modifier =
+ Modifier.align(Alignment.BottomCenter).padding(8.dp).graphicsLayer {
+ translationX = pagerState.offset.value
+ },
+ )
+ }
+ },
+ modifier = modifier.padding(8.dp).clip(roundedCornerShape),
+ ) { index ->
+ Card(
+ viewModel = viewModel.cards[index],
+ presentationStyle = presentationStyle,
+ modifier = Modifier.clip(roundedCornerShape),
+ )
+ }
+}
+
/** Renders the UI of a single media card. */
@Composable
private fun Card(
@@ -139,7 +258,7 @@ private fun Card(
Box(modifier) {
if (stlState.currentScene != Media.Scenes.Compact) {
- CardBackground(imageLoader = viewModel.artLoader, modifier = Modifier.matchParentSize())
+ CardBackground(image = viewModel.background, modifier = Modifier.matchParentSize())
}
key(stlState) {
@@ -158,6 +277,22 @@ private fun Card(
}
}
+@Composable
+private fun rememberAnimatedColorScheme(colorScheme: MediaColorScheme): AnimatedColorScheme {
+ val animatedPrimary by animateColorAsState(targetValue = colorScheme.primary)
+ val animatedOnPrimary by animateColorAsState(targetValue = colorScheme.onPrimary)
+
+ return remember {
+ object : AnimatedColorScheme {
+ override val primary: Color
+ get() = animatedPrimary
+
+ override val onPrimary: Color
+ get() = animatedOnPrimary
+ }
+ }
+}
+
/**
* Renders the foreground of a card, including all UI content and the internal "guts".
*
@@ -180,6 +315,8 @@ private fun ContentScope.CardForeground(
val isGutsVisible = viewModel.guts.isVisible
LaunchedEffect(isGutsVisible) { gutsAlphaAnimatable.animateTo(if (isGutsVisible) 1f else 0f) }
+ val colorScheme = rememberAnimatedColorScheme(viewModel.colorScheme)
+
// Use a custom layout to measure the content even if the content is being hidden because the
// internal guts are showing. This is needed because only the content knows the size the of the
// card and the guts are set to be the same size of the content.
@@ -189,6 +326,7 @@ private fun ContentScope.CardForeground(
viewModel = viewModel,
threeRows = threeRows,
fillHeight = fillHeight,
+ colorScheme = colorScheme,
modifier =
Modifier.graphicsLayer {
compositingStrategy = CompositingStrategy.ModulateAlpha
@@ -198,6 +336,7 @@ private fun ContentScope.CardForeground(
CardGuts(
viewModel = viewModel.guts,
+ colorScheme = colorScheme,
modifier =
Modifier.graphicsLayer {
compositingStrategy = CompositingStrategy.ModulateAlpha
@@ -232,6 +371,7 @@ private fun ContentScope.CardForegroundContent(
viewModel: MediaCardViewModel,
threeRows: Boolean,
fillHeight: Boolean,
+ colorScheme: AnimatedColorScheme,
modifier: Modifier = Modifier,
) {
Column(
@@ -249,12 +389,16 @@ private fun ContentScope.CardForegroundContent(
// Icon.
Icon(
icon = viewModel.icon,
- tint = LocalAndroidColorScheme.current.primaryFixed,
+ tint = colorScheme.primary,
modifier = Modifier.size(24.dp).clip(CircleShape),
)
Spacer(modifier = Modifier.weight(1f))
viewModel.outputSwitcherChips.fastForEach { chip ->
- OutputSwitcherChip(viewModel = chip, modifier = Modifier.padding(start = 8.dp))
+ OutputSwitcherChip(
+ viewModel = chip,
+ colorScheme = colorScheme,
+ modifier = Modifier.padding(start = 8.dp),
+ )
}
}
@@ -280,14 +424,16 @@ private fun ContentScope.CardForegroundContent(
modifier = Modifier.weight(1f).padding(end = 8.dp),
)
- AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) {
- PlayPauseAction(
- viewModel = viewModel.playPauseAction,
- buttonWidth = 48.dp,
- buttonColor = LocalAndroidColorScheme.current.primaryFixed,
- iconColor = LocalAndroidColorScheme.current.onPrimaryFixed,
- buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
- )
+ if (viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause) {
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
+ PlayPauseAction(
+ viewModel = checkNotNull(viewModel.playPauseAction),
+ buttonWidth = 48.dp,
+ buttonColor = colorScheme.primary,
+ iconColor = colorScheme.onPrimary,
+ buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
+ )
+ }
}
}
@@ -297,7 +443,14 @@ private fun ContentScope.CardForegroundContent(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = 24.dp),
) {
- Navigation(viewModel = viewModel.seekBar, isSeekBarVisible = true)
+ Navigation(
+ viewModel = viewModel.navigation,
+ isSeekBarVisible = true,
+ areActionsVisible =
+ viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause,
+ modifier = Modifier.weight(1f),
+ )
+
viewModel.additionalActions.fastForEachIndexed { index, action ->
SecondaryAction(
viewModel = action,
@@ -321,18 +474,34 @@ private fun ContentScope.CardForegroundContent(
)
Navigation(
- viewModel = viewModel.seekBar,
+ viewModel = viewModel.navigation,
isSeekBarVisible = false,
+ areActionsVisible =
+ viewModel.actionButtonLayout == MediaCardActionButtonLayout.WithPlayPause,
modifier = Modifier.padding(end = 8.dp),
)
- PlayPauseAction(
- viewModel = viewModel.playPauseAction,
- buttonWidth = 48.dp,
- buttonColor = LocalAndroidColorScheme.current.primaryFixed,
- iconColor = LocalAndroidColorScheme.current.onPrimaryFixed,
- buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
- )
+ if (
+ viewModel.actionButtonLayout == MediaCardActionButtonLayout.SecondaryActionsOnly
+ ) {
+ viewModel.additionalActions.fastForEachIndexed { index, action ->
+ SecondaryAction(
+ viewModel = action,
+ element = Media.Elements.additionalActionButton(index),
+ modifier = Modifier.padding(end = 8.dp),
+ )
+ }
+ }
+
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
+ PlayPauseAction(
+ viewModel = checkNotNull(viewModel.playPauseAction),
+ buttonWidth = 48.dp,
+ buttonColor = colorScheme.primary,
+ iconColor = colorScheme.onPrimary,
+ buttonCornerRadius = { isPlaying -> if (isPlaying) 16.dp else 48.dp },
+ )
+ }
}
}
}
@@ -375,18 +544,18 @@ private fun ContentScope.CompactCardForeground(
iconColor = MaterialTheme.colorScheme.onSurface,
)
- val nextAction = (viewModel.seekBar as? MediaSeekBarViewModel.Showing)?.next
- if (nextAction != null) {
+ val rightAction = (viewModel.navigation as? MediaNavigationViewModel.Showing)?.right
+ if (rightAction != null) {
SecondaryAction(
- viewModel = nextAction,
+ viewModel = rightAction,
element = Media.Elements.NextButton,
iconColor = MaterialTheme.colorScheme.onSurface,
)
}
- AnimatedVisibility(visible = viewModel.playPauseAction.isVisible) {
+ AnimatedVisibility(visible = viewModel.playPauseAction != null) {
PlayPauseAction(
- viewModel = viewModel.playPauseAction,
+ viewModel = checkNotNull(viewModel.playPauseAction),
buttonWidth = 72.dp,
buttonColor = MaterialTheme.colorScheme.primaryContainer,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer,
@@ -398,47 +567,38 @@ private fun ContentScope.CompactCardForeground(
/** Renders the background of a card, loading the artwork and showing an overlay on top of it. */
@Composable
-private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Modifier = Modifier) {
- var image: ImageBitmap? by remember { mutableStateOf(null) }
- LaunchedEffect(imageLoader) {
- image = null
- image = imageLoader()
- }
-
- val gradientBaseColor = MaterialTheme.colorScheme.onSurface
- Box(
- modifier =
- modifier.drawWithContent {
- // Draw the content of the box (loaded art or placeholder).
- drawContent()
-
- if (image != null) {
- // Then draw the overlay.
- drawRect(
- brush =
- Brush.radialGradient(
- 0f to gradientBaseColor.copy(alpha = 0.65f),
- 1f to gradientBaseColor.copy(alpha = 0.75f),
- center = size.center,
- radius = max(size.width, size.height) / 2,
- )
- )
- }
- }
- ) {
- image?.let { loadedImage ->
+private fun CardBackground(image: ImageBitmap?, modifier: Modifier = Modifier) {
+ Crossfade(targetState = image, modifier = modifier) { imageOrNull ->
+ if (imageOrNull != null) {
// Loaded art.
+ val gradientBaseColor = MaterialTheme.colorScheme.onSurface
Image(
- bitmap = loadedImage,
+ bitmap = imageOrNull,
contentDescription = null,
contentScale = ContentScale.Crop,
- modifier = Modifier.matchParentSize(),
+ modifier =
+ Modifier.fillMaxSize().drawWithContent {
+ // Draw the content (loaded art).
+ drawContent()
+
+ if (image != null) {
+ // Then draw the overlay.
+ drawRect(
+ brush =
+ Brush.radialGradient(
+ 0f to gradientBaseColor.copy(alpha = 0.65f),
+ 1f to gradientBaseColor.copy(alpha = 0.75f),
+ center = size.center,
+ radius = max(size.width, size.height) / 2,
+ )
+ )
+ }
+ },
)
+ } else {
+ // Placeholder.
+ Box(Modifier.background(MaterialTheme.colorScheme.onSurface).fillMaxSize())
}
- ?: run {
- // Placeholder.
- Box(Modifier.background(MaterialTheme.colorScheme.onSurface).matchParentSize())
- }
}
}
@@ -449,22 +609,26 @@ private fun CardBackground(imageLoader: suspend () -> ImageBitmap, modifier: Mod
* would otherwise be showing based on the view-model alone. This is meant for callers to decide
* whether they'd like to show the seek bar in addition to the prev/next buttons or just show the
* buttons.
+ *
+ * If [areActionsVisible] is `false`, the left/right buttons to the left and right of the seek bar
+ * will not be included in the layout.
*/
@Composable
private fun ContentScope.Navigation(
- viewModel: MediaSeekBarViewModel,
+ viewModel: MediaNavigationViewModel,
isSeekBarVisible: Boolean,
+ areActionsVisible: Boolean,
modifier: Modifier = Modifier,
) {
when (viewModel) {
- is MediaSeekBarViewModel.Showing -> {
+ is MediaNavigationViewModel.Showing -> {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
- viewModel.previous?.let {
- SecondaryAction(viewModel = it, element = Media.Elements.PrevButton)
+ if (areActionsVisible) {
+ SecondaryAction(viewModel = viewModel.left, element = Media.Elements.PrevButton)
}
val interactionSource = remember { MutableInteractionSource() }
@@ -499,13 +663,16 @@ private fun ContentScope.Navigation(
}
}
- viewModel.next?.let {
- SecondaryAction(viewModel = it, element = Media.Elements.NextButton)
+ if (areActionsVisible) {
+ SecondaryAction(
+ viewModel = viewModel.right,
+ element = Media.Elements.NextButton,
+ )
}
}
}
- is MediaSeekBarViewModel.Hidden -> Unit
+ is MediaNavigationViewModel.Hidden -> Unit
}
}
@@ -647,7 +814,11 @@ private fun SeekBarTrack(
/** Renders the internal "guts" of a card. */
@Composable
-private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Modifier) {
+private fun CardGuts(
+ viewModel: MediaCardGutsViewModel,
+ colorScheme: AnimatedColorScheme,
+ modifier: Modifier = Modifier,
+) {
Box(
modifier =
modifier.pointerInput(Unit) { detectLongPressGesture { viewModel.onLongClick() } }
@@ -682,7 +853,7 @@ private fun CardGuts(viewModel: MediaCardGutsViewModel, modifier: Modifier = Mod
) {
Text(
text = checkNotNull(viewModel.primaryAction.text),
- color = LocalAndroidColorScheme.current.onPrimaryFixed,
+ color = colorScheme.onPrimary,
)
}
@@ -740,28 +911,22 @@ private fun ContentScope.Metadata(
@Composable
private fun OutputSwitcherChip(
viewModel: MediaOutputSwitcherChipViewModel,
+ colorScheme: AnimatedColorScheme,
modifier: Modifier = Modifier,
) {
PlatformButton(
onClick = viewModel.onClick,
- colors =
- ButtonDefaults.buttonColors(
- containerColor = LocalAndroidColorScheme.current.primaryFixed
- ),
+ colors = ButtonDefaults.buttonColors(containerColor = colorScheme.primary),
contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
modifier = modifier.height(24.dp),
) {
- Icon(
- icon = viewModel.icon,
- tint = LocalAndroidColorScheme.current.onPrimaryFixed,
- modifier = Modifier.size(16.dp),
- )
+ Icon(icon = viewModel.icon, tint = colorScheme.onPrimary, modifier = Modifier.size(16.dp))
viewModel.text?.let {
Spacer(Modifier.size(4.dp))
Text(
text = viewModel.text,
style = MaterialTheme.typography.bodySmall,
- color = LocalAndroidColorScheme.current.onPrimaryFixed,
+ color = colorScheme.onPrimary,
)
}
}
@@ -785,7 +950,8 @@ private fun ContentScope.PlayPauseAction(
// This element can be animated when switching between scenes inside a media card.
Element(key = Media.Elements.PlayPauseButton, modifier = modifier) {
PlatformButton(
- onClick = viewModel.onClick,
+ onClick = viewModel.onClick ?: {},
+ enabled = viewModel.onClick != null,
colors = ButtonDefaults.buttonColors(containerColor = buttonColor),
shape = RoundedCornerShape(cornerRadius),
modifier = Modifier.size(width = buttonWidth, height = 48.dp),
@@ -793,22 +959,29 @@ private fun ContentScope.PlayPauseAction(
when (viewModel.state) {
is MediaSessionState.Playing,
is MediaSessionState.Paused -> {
- // TODO(b/399860531): load this expensive-to-load animated vector drawable off
- // the main thread.
- val iconResource = checkNotNull(viewModel.icon)
- Icon(
- painter =
- rememberAnimatedVectorPainter(
- animatedImageVector =
- AnimatedImageVector.animatedVectorResource(
- id = iconResource.res
- ),
- atEnd = viewModel.state == MediaSessionState.Playing,
- ),
- contentDescription = iconResource.contentDescription?.load(),
- tint = iconColor,
- modifier = Modifier.size(24.dp),
- )
+ val painterOrNull =
+ when (viewModel.icon) {
+ // TODO(b/399860531): load this expensive-to-load animated vector
+ // drawable off the main thread.
+ is Icon.Resource ->
+ rememberAnimatedVectorPainter(
+ animatedImageVector =
+ AnimatedImageVector.animatedVectorResource(
+ id = viewModel.icon.res
+ ),
+ atEnd = viewModel.state == MediaSessionState.Playing,
+ )
+ is Icon.Loaded -> rememberDrawablePainter(viewModel.icon.drawable)
+ null -> null
+ }
+ painterOrNull?.let { painter ->
+ Icon(
+ painter = painter,
+ contentDescription = viewModel.icon?.contentDescription?.load(),
+ tint = iconColor,
+ modifier = Modifier.size(24.dp),
+ )
+ }
}
is MediaSessionState.Buffering -> {
CircularProgressIndicator(color = iconColor, modifier = Modifier.size(24.dp))
@@ -831,7 +1004,7 @@ private fun ContentScope.SecondaryAction(
element: ElementKey? = null,
iconColor: Color = Color.White,
) {
- if (element != null) {
+ if (viewModel !is MediaSecondaryActionViewModel.None && element != null) {
Element(key = element, modifier = modifier) {
SecondaryActionContent(viewModel = viewModel, iconColor = iconColor)
}
@@ -847,14 +1020,22 @@ private fun SecondaryActionContent(
iconColor: Color,
modifier: Modifier = Modifier,
) {
- PlatformIconButton(
- onClick = viewModel.onClick,
- iconResource = (viewModel.icon as Icon.Resource).res,
- contentDescription = viewModel.icon.contentDescription?.load(),
- colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor),
- enabled = viewModel.isEnabled,
- modifier = modifier.size(48.dp).padding(13.dp),
- )
+ val sharedModifier = modifier.size(48.dp).padding(13.dp)
+ when (viewModel) {
+ is MediaSecondaryActionViewModel.Action ->
+ PlatformIconButton(
+ onClick = viewModel.onClick ?: {},
+ iconResource = (viewModel.icon as Icon.Resource).res,
+ contentDescription = viewModel.icon.contentDescription?.load(),
+ colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor),
+ enabled = viewModel.onClick != null,
+ modifier = sharedModifier,
+ )
+
+ is MediaSecondaryActionViewModel.ReserveSpace -> Spacer(modifier = sharedModifier)
+
+ is MediaSecondaryActionViewModel.None -> Unit
+ }
}
/** Enumerates all supported media presentation styles. */
@@ -867,6 +1048,19 @@ enum class MediaPresentationStyle {
Compact,
}
+data class MediaUiBehavior(
+ val isCarouselDismissible: Boolean = true,
+ val isCarouselScrollingEnabled: Boolean = true,
+ val carouselVisibility: MediaCarouselVisibility = MediaCarouselVisibility.WhenNotEmpty,
+ val isFalsingProtectionNeeded: Boolean = false,
+)
+
+@Stable
+private interface AnimatedColorScheme {
+ val primary: Color
+ val onPrimary: Color
+}
+
private object Media {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt
index ecd6e6d094d9..833a04ddcb55 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt
@@ -19,6 +19,8 @@ package com.android.systemui.media.remedia.ui.viewmodel
import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.ImageBitmap
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.media.remedia.shared.model.MediaCardActionButtonLayout
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
/** Models UI state for a media card. */
@Stable
@@ -31,20 +33,19 @@ interface MediaCardViewModel {
val icon: Icon
- /**
- * A callback to load the artwork for the media shown on this card. This callback will be
- * invoked on the main thread, it's up to the implementation to move the loading off the main
- * thread.
- */
- val artLoader: suspend () -> ImageBitmap
+ val background: ImageBitmap?
+
+ val colorScheme: MediaColorScheme
val title: String
val subtitle: String
- val playPauseAction: MediaPlayPauseActionViewModel
+ val actionButtonLayout: MediaCardActionButtonLayout
+
+ val playPauseAction: MediaPlayPauseActionViewModel?
- val seekBar: MediaSeekBarViewModel
+ val navigation: MediaNavigationViewModel
val additionalActions: List<MediaSecondaryActionViewModel>
@@ -53,7 +54,7 @@ interface MediaCardViewModel {
val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel>
/** Simple icon-only version of the output switcher for use in compact UIs. */
- val outputSwitcherChipButton: MediaSecondaryActionViewModel
+ val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action
val onClick: () -> Unit
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt
new file mode 100644
index 000000000000..53aa87ce0843
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCarouselVisibility.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.ui.viewmodel
+
+/** Enumerates the known rules for media carousel visibility. */
+enum class MediaCarouselVisibility {
+
+ /** The carousel should be shown as long as it has at least one card. */
+ WhenNotEmpty,
+
+ /**
+ * The carousel should be shown as long as it has at least one card that represents an active
+ * media session. In other words: if all cards in the carousel represent _inactive_ sessions,
+ * the carousel should _not_ be visible.
+ */
+ WhenAnyCardIsActive,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
index f1ced6bf908d..c34c7337290f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
@@ -18,17 +18,26 @@ package com.android.systemui.media.remedia.ui.viewmodel
import androidx.annotation.FloatRange
-/** Models UI state for the seek bar. */
-sealed interface MediaSeekBarViewModel {
+/**
+ * Models UI state for the navigation component of the UI (potentially containing the seek bar and
+ * the buttons to its left and right).
+ */
+sealed interface MediaNavigationViewModel {
/** The seek bar should be showing. */
data class Showing(
/** The progress to show on the seek bar, between `0` and `1`. */
@FloatRange(from = 0.0, to = 1.0) val progress: Float,
- /** The previous button; or `null` if it should be absent in the UI. */
- val previous: MediaSecondaryActionViewModel?,
- /** The next button; or `null` if it should be absent in the UI. */
- val next: MediaSecondaryActionViewModel?,
+ /**
+ * The action button to the left of the seek bar; or `null` if it should be absent in the
+ * UI.
+ */
+ val left: MediaSecondaryActionViewModel,
+ /**
+ * The action button to the right of the seek bar; or `null` if it should be absent in the
+ * UI.
+ */
+ val right: MediaSecondaryActionViewModel,
/**
* Whether the portion of the seek bar track before the thumb should show the squiggle
* animation.
@@ -50,8 +59,8 @@ sealed interface MediaSeekBarViewModel {
* the seek bar). The position/progress should be committed.
*/
val onScrubFinished: () -> Unit,
- ) : MediaSeekBarViewModel
+ ) : MediaNavigationViewModel
/** The seek bar should be hidden. */
- data object Hidden : MediaSeekBarViewModel
+ data object Hidden : MediaNavigationViewModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt
index 4cb11bc0b8d0..ecc92d778f8e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt
@@ -21,8 +21,7 @@ import com.android.systemui.media.remedia.shared.model.MediaSessionState
/** Models UI state for the play/pause action button within media controls. */
data class MediaPlayPauseActionViewModel(
- val isVisible: Boolean,
val state: MediaSessionState,
- val icon: Icon.Resource?,
- val onClick: () -> Unit,
+ val icon: Icon?,
+ val onClick: (() -> Unit)?,
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt
index a4806800a7b1..d28ca7ab7121 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt
@@ -19,8 +19,10 @@ package com.android.systemui.media.remedia.ui.viewmodel
import com.android.systemui.common.shared.model.Icon
/** Models UI state for a secondary action button within media controls. */
-data class MediaSecondaryActionViewModel(
- val icon: Icon,
- val isEnabled: Boolean,
- val onClick: () -> Unit,
-)
+sealed interface MediaSecondaryActionViewModel {
+ data class Action(val icon: Icon, val onClick: (() -> Unit)?) : MediaSecondaryActionViewModel
+
+ data object ReserveSpace : MediaSecondaryActionViewModel
+
+ data object None : MediaSecondaryActionViewModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
new file mode 100644
index 000000000000..b4f3d2724e75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.remedia.ui.viewmodel
+
+import android.content.Context
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.ImageBitmap
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.media.remedia.domain.interactor.MediaInteractor
+import com.android.systemui.media.remedia.domain.model.MediaActionModel
+import com.android.systemui.media.remedia.shared.model.MediaColorScheme
+import com.android.systemui.media.remedia.shared.model.MediaSessionState
+import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.math.roundToLong
+import kotlinx.coroutines.awaitCancellation
+
+/** Models UI state for a media element. */
+class MediaViewModel
+@AssistedInject
+constructor(
+ private val interactor: MediaInteractor,
+ @Assisted private val context: Context,
+ @Assisted private val carouselVisibility: MediaCarouselVisibility,
+) : ExclusiveActivatable() {
+
+ /** Whether the user is actively moving the thumb of the seek bar. */
+ private var isScrubbing: Boolean by mutableStateOf(false)
+ /** The position of the thumb of the seek bar as the user is scrubbing it. */
+ private var seekProgress: Float by mutableFloatStateOf(0f)
+ /** Whether the internal "guts" are visible. */
+ private var isGutsVisible: Boolean by mutableStateOf(false)
+ /** The index of the currently-selected card. */
+ private var selectedCardIndex: Int by mutableIntStateOf(0)
+ private set
+
+ /** The current list of cards to show in the UI. */
+ val cards: List<MediaCardViewModel> by derivedStateOf {
+ interactor.sessions.mapIndexed { sessionIndex, session ->
+ val isCurrentSessionAndScrubbing = isScrubbing && sessionIndex == selectedCardIndex
+ object : MediaCardViewModel {
+ override val key = session.key
+ override val icon = session.appIcon
+ override val background: ImageBitmap?
+ get() = session.background
+
+ override val colorScheme: MediaColorScheme
+ get() = session.colorScheme
+
+ override val title = session.title
+ override val subtitle = session.subtitle
+ override val actionButtonLayout = session.actionButtonLayout
+ override val playPauseAction =
+ session.playPauseAction.toPlayPauseActionViewModel(session.state)
+ override val additionalActions: List<MediaSecondaryActionViewModel>
+ get() {
+ return session.additionalActions.map { action ->
+ action.toSecondaryActionViewModel()
+ }
+ }
+
+ override val navigation: MediaNavigationViewModel
+ get() {
+ return if (session.canBeScrubbed) {
+ MediaNavigationViewModel.Showing(
+ progress =
+ if (!isCurrentSessionAndScrubbing) {
+ session.positionMs.toFloat() / session.durationMs
+ } else {
+ seekProgress
+ },
+ left = session.leftAction.toSecondaryActionViewModel(),
+ right = session.rightAction.toSecondaryActionViewModel(),
+ isSquiggly =
+ session.state != MediaSessionState.Paused &&
+ !isCurrentSessionAndScrubbing,
+ isScrubbing = isCurrentSessionAndScrubbing,
+ onScrubChange = { progress ->
+ check(selectedCardIndex == sessionIndex) {
+ "Can't seek on a card that's not the selected card!"
+ }
+ isScrubbing = true
+ seekProgress = progress
+ },
+ onScrubFinished = {
+ interactor.seek(
+ sessionKey = session.key,
+ to = (seekProgress * session.durationMs).roundToLong(),
+ )
+ isScrubbing = false
+ },
+ )
+ } else {
+ MediaNavigationViewModel.Hidden
+ }
+ }
+
+ override val guts: MediaCardGutsViewModel
+ get() {
+ return MediaCardGutsViewModel(
+ isVisible = isGutsVisible,
+ text =
+ if (session.canBeHidden) {
+ context.getString(
+ R.string.controls_media_close_session,
+ session.appName,
+ )
+ } else {
+ context.getString(R.string.controls_media_active_session)
+ },
+ primaryAction =
+ if (session.canBeHidden) {
+ MediaGutsButtonViewModel(
+ text =
+ context.getString(
+ R.string.controls_media_dismiss_button
+ ),
+ onClick = {
+ interactor.hide(session.key)
+ isGutsVisible = false
+ },
+ )
+ } else {
+ MediaGutsButtonViewModel(
+ text = context.getString(R.string.cancel),
+ onClick = { isGutsVisible = false },
+ )
+ },
+ secondaryAction =
+ MediaGutsButtonViewModel(
+ text = context.getString(R.string.cancel),
+ onClick = { isGutsVisible = false },
+ )
+ .takeIf { session.canBeHidden },
+ settingsButton =
+ MediaGutsSettingsButtonViewModel(
+ icon =
+ Icon.Resource(
+ res = R.drawable.ic_settings,
+ contentDescription =
+ ContentDescription.Resource(
+ res = R.string.controls_media_settings_button
+ ),
+ ),
+ onClick = { interactor.openMediaSettings() },
+ ),
+ onLongClick = { isGutsVisible = false },
+ )
+ }
+
+ override val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel>
+ get() {
+ return listOf(
+ MediaOutputSwitcherChipViewModel(
+ icon = session.outputDevice.icon,
+ text = session.outputDevice.name,
+ onClick = {
+ // TODO(b/397989775): tell the UI to show the output switcher.
+ },
+ )
+ )
+ }
+
+ override val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action
+ get() {
+ return MediaSecondaryActionViewModel.Action(
+ icon = session.outputDevice.icon,
+ onClick = {
+ // TODO(b/397989775): tell the UI to show the output switcher.
+ },
+ )
+ }
+
+ override val onClick = session.onClick
+ override val onClickLabel =
+ context.getString(R.string.controls_media_playing_item_description)
+ override val onLongClick = { isGutsVisible = true }
+ }
+ }
+ }
+
+ /** Whether the carousel should be visible. */
+ val isCarouselVisible: Boolean
+ get() =
+ when (carouselVisibility) {
+ MediaCarouselVisibility.WhenNotEmpty -> interactor.sessions.isNotEmpty()
+
+ MediaCarouselVisibility.WhenAnyCardIsActive ->
+ interactor.sessions.any { session -> session.isActive }
+ }
+
+ /** Notifies that the card at [cardIndex] has been selected in the UI. */
+ fun onCardSelected(cardIndex: Int) {
+ check(cardIndex >= 0 && cardIndex < cards.size)
+ selectedCardIndex = cardIndex
+ }
+
+ override suspend fun onActivated(): Nothing {
+ awaitCancellation()
+ }
+
+ private fun MediaActionModel.toPlayPauseActionViewModel(
+ mediaSessionState: MediaSessionState
+ ): MediaPlayPauseActionViewModel? {
+ return when (this) {
+ is MediaActionModel.Action ->
+ MediaPlayPauseActionViewModel(
+ state = mediaSessionState,
+ icon = icon,
+ onClick = onClick ?: {},
+ )
+ is MediaActionModel.None,
+ is MediaActionModel.ReserveSpace -> null
+ }
+ }
+
+ private fun MediaActionModel.toSecondaryActionViewModel(): MediaSecondaryActionViewModel {
+ return when (this) {
+ is MediaActionModel.Action ->
+ MediaSecondaryActionViewModel.Action(icon = icon, onClick = onClick)
+ is MediaActionModel.ReserveSpace -> MediaSecondaryActionViewModel.ReserveSpace
+ is MediaActionModel.None -> MediaSecondaryActionViewModel.None
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index f1f5b267f9c1..8920c86282da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.composefragment
import android.annotation.SuppressLint
import android.content.Context
+import android.content.res.Configuration
import android.graphics.PointF
import android.graphics.Rect
import android.os.Bundle
@@ -48,6 +49,7 @@ import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -58,6 +60,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
@@ -69,6 +72,8 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.layout.positionOnScreen
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
@@ -102,6 +107,7 @@ import com.android.mechanics.GestureContext
import com.android.systemui.Dumpable
import com.android.systemui.Flags
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyboard.shortcut.ui.composable.InteractionsConfig
@@ -249,7 +255,7 @@ constructor(
@Composable
private fun Content() {
- PlatformTheme(isDarkTheme = true) {
+ PlatformTheme(isDarkTheme = true /* Delete AlwaysDarkMode when removing this */) {
ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) {
// TODO(b/389985793): Make sure that there is no coroutine work or recompositions
// happening when alwaysCompose is true but isQsVisibleAndAnyShadeExpanded is false.
@@ -740,17 +746,26 @@ constructor(
)
val BrightnessSlider =
@Composable {
- BrightnessSliderContainer(
- viewModel = containerViewModel.brightnessSliderViewModel,
- modifier =
+ AlwaysDarkMode {
+ Box(
Modifier.systemGestureExclusionInShade(
- enabled = {
- layoutState.transitionState is
- TransitionState.Idle
- }
- )
- .fillMaxWidth(),
- )
+ enabled = {
+ layoutState.transitionState is TransitionState.Idle
+ }
+ )
+ ) {
+ BrightnessSliderContainer(
+ viewModel =
+ containerViewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors(
+ Color.Transparent,
+ ContainerColors.defaultContainerColor,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ }
}
val TileGrid =
@Composable {
@@ -1226,3 +1241,28 @@ private fun interactionsConfig() =
private inline val alwaysCompose
get() = Flags.alwaysComposeQsUiFragment()
+
+/**
+ * Forces the configuration and themes to be dark theme. This is needed in order to have
+ * [colorResource] retrieve the dark mode colors.
+ *
+ * This should be removed when we remove the force dark mode in [PlatformTheme] at the root of the
+ * compose hierarchy.
+ */
+@Composable
+private fun AlwaysDarkMode(content: @Composable () -> Unit) {
+ val currentConfig = LocalConfiguration.current
+ val darkConfig =
+ Configuration(currentConfig).apply {
+ uiMode =
+ (uiMode and (Configuration.UI_MODE_NIGHT_MASK.inv())) or
+ Configuration.UI_MODE_NIGHT_YES
+ }
+ val newContext = LocalContext.current.createConfigurationContext(darkConfig)
+ CompositionLocalProvider(
+ LocalConfiguration provides darkConfig,
+ LocalContext provides newContext,
+ ) {
+ content()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 153238fc91c9..a66b51f6fe50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -59,12 +59,14 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
import com.android.compose.modifiers.size
import com.android.compose.modifiers.thenIf
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD
+import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeIconSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeXOffset
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeYOffset
@@ -149,16 +151,14 @@ fun InteractiveTileContainer(
onClick = onClick,
)
) {
+ val size = with(LocalDensity.current) { BadgeIconSize.toDp() }
Icon(
Icons.Default.Remove,
contentDescription = null,
modifier =
- Modifier.size(
- width = { decorationSize.width.roundToInt() },
- height = { decorationSize.height.roundToInt() },
- )
- .align(Alignment.Center)
- .graphicsLayer { this.alpha = badgeIconAlpha },
+ Modifier.size(size).align(Alignment.Center).graphicsLayer {
+ this.alpha = badgeIconAlpha
+ },
)
}
}
@@ -219,12 +219,13 @@ fun StaticTileBadge(
}
) {
val secondaryColor = MaterialTheme.colorScheme.secondary
+ val size = with(LocalDensity.current) { BadgeIconSize.toDp() }
Icon(
icon,
contentDescription = contentDescription,
modifier =
- Modifier.size(BadgeSize).align(Alignment.Center).drawBehind {
- drawCircle(secondaryColor)
+ Modifier.size(size).align(Alignment.Center).drawBehind {
+ drawCircle(secondaryColor, radius = BadgeSize.toPx() / 2)
},
)
}
@@ -338,6 +339,7 @@ private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset
private object SelectionDefaults {
val SelectedBorderWidth = 2.dp
val BadgeSize = 24.dp
+ val BadgeIconSize = 16.sp
val BadgeXOffset = -4.dp
val BadgeYOffset = 4.dp
val ResizingPillWidth = 8.dp
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
index 5e7172ee3ba7..268efcef9062 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
@@ -21,6 +21,7 @@ import android.provider.Settings
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager
import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
@@ -37,6 +38,7 @@ constructor(
@Main private val mainContext: CoroutineContext,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
private val hearingDevicesDialogManager: HearingDevicesDialogManager,
+ private val settingsPackageRepository: QSSettingsPackageRepository,
) : QSTileUserActionInteractor<HearingDevicesTileModel> {
override suspend fun handleInput(input: QSTileInput<HearingDevicesTileModel>) =
@@ -53,7 +55,8 @@ constructor(
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
action.expandable,
- Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS),
+ Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS)
+ .setPackage(settingsPackageRepository.getSettingsPackageName()),
)
}
is QSTileUserAction.ToggleClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt
index 22449c73c921..962a3bd2376b 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ComposeDialogComposableProvider.kt
@@ -25,6 +25,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import com.android.compose.theme.PlatformTheme
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
+import com.android.systemui.brightness.ui.compose.ContainerColors
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.lifecycle.rememberViewModel
@@ -46,7 +47,11 @@ private fun BrightnessSliderForDialog(
rememberViewModel(traceName = "BrightnessDialog.viewModel") {
brightnessSliderViewModelFactory.create(false)
}
- BrightnessSliderContainer(viewModel = viewModel, Modifier.fillMaxWidth().padding(8.dp))
+ BrightnessSliderContainer(
+ viewModel = viewModel,
+ containerColors = ContainerColors.singleColor(ContainerColors.defaultContainerColor),
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ )
}
class ComposableProvider(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 20b44d73e097..5609326362fc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.compose.animation.scene.OverlayKey
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -86,6 +87,22 @@ constructor(
(ViewGroup, StatusBarLocation) -> BatteryMeterViewController =
batteryMeterViewControllerFactory::create
+ val showClock: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "showClock",
+ initialValue =
+ shouldShowClock(
+ isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value,
+ overlays = sceneInteractor.currentOverlays.value,
+ ),
+ source =
+ combine(
+ shadeInteractor.isShadeLayoutWide,
+ sceneInteractor.currentOverlays,
+ ::shouldShowClock,
+ ),
+ )
+
val notificationsChipHighlight: HeaderChipHighlight by
hydrator.hydratedStateOf(
traceName = "notificationsChipHighlight",
@@ -114,13 +131,6 @@ constructor(
},
)
- val isShadeLayoutWide: Boolean by
- hydrator.hydratedStateOf(
- traceName = "isShadeLayoutWide",
- initialValue = shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide,
- )
-
/** True if there is exactly one mobile connection. */
val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
@@ -271,6 +281,11 @@ constructor(
}
}
+ private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean {
+ // Notifications shade on narrow layout renders its own clock. Hide the header clock.
+ return isShadeLayoutWide || Overlays.NotificationsShade !in overlays
+ }
+
private fun getFormatFromPattern(pattern: String?): DateFormat {
val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault())
format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 434eb7d3d410..a7929ecbd801 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -33,6 +33,7 @@ import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
@@ -44,7 +45,6 @@ import com.android.systemui.shade.ShadeSurfaceImpl;
import com.android.systemui.shade.carrier.ShadeCarrierGroupController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationClickNotifier;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
index c3266fc57c2e..92c87e6403ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt
@@ -18,10 +18,10 @@ package com.android.systemui.statusbar.notification.init
import android.service.notification.StatusBarNotification
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
import com.android.systemui.statusbar.NotificationListener
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.AnimatedImageNotificationManager
import com.android.systemui.statusbar.notification.NotificationActivityStarter
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 76d8fb8d3c15..689222608abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -128,7 +128,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
updateColors();
}
- private void updateColors() {
+ protected void updateColors() {
if (notificationRowTransparency()) {
if (mIsBlurSupported) {
mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
index 640d364895ae..dccc28f5fe49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt
@@ -34,11 +34,11 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
-import android.widget.Switch
import android.widget.TextView
import com.android.settingslib.Utils
import com.android.systemui.res.R
import com.android.systemui.util.Assert
+import com.google.android.material.materialswitch.MaterialSwitch
/** Half-shelf for notification channel controls */
class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
@@ -139,12 +139,12 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a
class AppControlView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
lateinit var iconView: ImageView
lateinit var channelName: TextView
- lateinit var switch: Switch
+ lateinit var switch: MaterialSwitch
override fun onFinishInflate() {
iconView = requireViewById(R.id.icon)
channelName = requireViewById(R.id.app_name)
- switch = requireViewById(R.id.toggle)
+ switch = requireViewById(R.id.material_toggle)
setOnClickListener { switch.toggle() }
}
@@ -155,7 +155,7 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
lateinit var controller: ChannelEditorDialogController
private lateinit var channelName: TextView
private lateinit var channelDescription: TextView
- private lateinit var switch: Switch
+ private lateinit var switch: MaterialSwitch
private val highlightColor: Int
var gentle = false
@@ -175,7 +175,7 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) {
super.onFinishInflate()
channelName = requireViewById(R.id.channel_name)
channelDescription = requireViewById(R.id.channel_description)
- switch = requireViewById(R.id.toggle)
+ switch = requireViewById(R.id.material_toggle)
switch.setOnCheckedChangeListener { _, b ->
channel?.let {
controller.proposeEditForChannel(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 4e21885d7b78..86509a91f134 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
@@ -114,7 +114,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
@@ -123,6 +122,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.logging.NotificationCounters;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
@@ -185,7 +185,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mShowSnooze = false;
private boolean mIsFaded;
- private boolean mIsPromotedOngoing = false;
private boolean mHasStatusBarChipDuringHeadsUpAnimation = false;
@Nullable
@@ -871,7 +870,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private void updateLimitsForView(NotificationContentView layout) {
final int maxExpandedHeight;
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing;
} else {
maxExpandedHeight = mMaxExpandedHeight;
@@ -979,7 +978,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
} else if (isAboveShelf() != wasAboveShelf) {
mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
}
- updateBackgroundOpacity();
+ updateColors();
}
/**
@@ -1350,7 +1349,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren) {
return mChildrenContainer.getIntrinsicHeight();
}
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return getMaxExpandHeight();
}
if (mExpandedWhenPinned) {
@@ -2939,7 +2938,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren && !shouldShowPublic()) {
return !mChildrenExpanded;
}
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return false;
}
return mEnableNonGroupedNotificationExpand && mExpandable;
@@ -2950,17 +2949,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPrivateLayout.updateExpandButtons(isExpandable());
}
- /**
- * Set this notification to be promoted ongoing
- */
- public void setPromotedOngoing(boolean promotedOngoing) {
- if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) {
- return;
- }
-
- mIsPromotedOngoing = promotedOngoing;
- setExpandable(!mIsPromotedOngoing);
- }
/**
* Sets whether the status bar is showing a chip corresponding to this notification.
@@ -3061,7 +3049,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public void setUserLocked(boolean userLocked) {
- if (isPromotedOngoing()) return;
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return;
mUserLocked = userLocked;
mPrivateLayout.setUserExpanding(userLocked);
@@ -3121,7 +3109,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mChildrenContainer.setOnKeyguard(onKeyguard);
}
}
- updateBackgroundOpacity();
+ updateColors();
}
}
@@ -3247,7 +3235,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isPromotedOngoing() {
- return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing;
+ if (!PromotedNotificationUi.isEnabled()) {
+ return false;
+ }
+
+ final NotificationEntry entry = mEntry;
+ if (entry == null) {
+ return false;
+ }
+
+ return entry.isPromotedOngoing();
}
private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) {
@@ -3309,7 +3306,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isExpanded(boolean allowOnKeyguard) {
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return isPromotedNotificationExpanded(allowOnKeyguard);
}
@@ -4396,9 +4393,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
+ (!shouldShowPublic() && mIsSummaryWithChildren));
pw.print(", mShowNoBackground: " + mShowNoBackground);
pw.print(", clipBounds: " + getClipBounds());
- if (PromotedNotificationUiForceExpanded.isEnabled()) {
- pw.print(", isPromotedOngoing: " + isPromotedOngoing());
- }
+ pw.print(", isPromotedOngoing: " + isPromotedOngoing());
if (notificationRowAccessibilityExpanded()) {
pw.print(", isShowingExpanded: " + isShowingExpanded());
pw.print(", isAccessibilityExpandable: " + isAccessibilityExpandable());
@@ -4630,11 +4625,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
- private void updateBackgroundOpacity() {
- if (mBackgroundNormal != null) {
- // Row background should be opaque when it's displayed as a heads-up notification or
- // displayed on keyguard.
- mBackgroundNormal.setForceOpaque(mIsHeadsUp || mOnKeyguard);
- }
+ @Override
+ protected boolean usesTransparentBackground() {
+ return super.usesTransparentBackground() && !mIsHeadsUp && !mOnKeyguard;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index e1219e88a405..4914e1073059 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -39,7 +39,6 @@ import androidx.annotation.Nullable;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Dumpable;
-import com.android.systemui.common.shared.colors.SurfaceEffectColors;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.shared.NotificationAddXOnHoverToDismiss;
import com.android.systemui.util.DrawableDumpKt;
@@ -156,14 +155,6 @@ public class NotificationBackgroundView extends View implements Dumpable,
mBgIsColorized = b;
}
- /** Sets if the background should be opaque. */
- public void setForceOpaque(boolean forceOpaque) {
- mForceOpaque = forceOpaque;
- if (notificationRowTransparency()) {
- updateBaseLayerColor();
- }
- }
-
private Path calculateDismissButtonCutoutPath(Rect backgroundBounds) {
// TODO(b/365585705): Adapt to RTL after the UX design is finalized.
@@ -327,10 +318,7 @@ public class NotificationBackgroundView extends View implements Dumpable,
// For colorized notifications, this uses a color that matches the tint color at 90% alpha.
int color = isColorized()
? ColorUtils.setAlphaComponent(mTintColor, (int) (MAX_ALPHA * 0.9f))
- : SurfaceEffectColors.surfaceEffect1(getContext());
- if (mForceOpaque) {
- color = ColorUtils.setAlphaComponent(color, MAX_ALPHA);
- }
+ : mNormalColor;
getBaseBackgroundLayer().setColorFilter(
new PorterDuffColorFilter(
color,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 51569c1596ef..3ffc052b5acc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -59,7 +59,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
@@ -1006,10 +1005,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
entry.setPromotedNotificationContentModel(result.mPromotedContent);
}
- if (PromotedNotificationUiForceExpanded.isEnabled()) {
- row.setPromotedOngoing(entry.isOngoingPromoted());
- }
-
boolean setRepliesAndActions = true;
if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
if (result.inflatedContentView != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 482b315aa14d..509f836ffb8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -53,7 +53,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
@@ -1520,10 +1519,6 @@ constructor(
entry.promotedNotificationContentModel = result.promotedContent
}
- if (PromotedNotificationUiForceExpanded.isEnabled) {
- row.setPromotedOngoing(entry.isOngoingPromoted())
- }
-
result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
setContentViewsFromRemoteViews(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index f00c3ae20e30..d7dd7ec08583 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.shared
import android.app.PendingIntent
import android.graphics.drawable.Icon
-import android.util.Log
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.stack.PriorityBucket
@@ -86,14 +85,6 @@ data class ActiveNotificationModel(
*/
val promotedContent: PromotedNotificationContentModel?,
) : ActiveNotificationEntryModel() {
- init {
- if (!PromotedNotificationContentModel.featureFlagEnabled()) {
- if (promotedContent != null) {
- Log.wtf(TAG, "passing non-null promoted content without feature flag enabled")
- }
- }
- }
-
companion object {
private const val TAG = "ActiveNotificationEntryModel"
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 9aa4c54c4292..e4e56c5de65b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -58,10 +58,10 @@ import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.log.SessionTracker;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.model.Scenes;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.KeyguardStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index fc721bfae369..a4ee4ad6f6ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -140,6 +140,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.views.NavigationBarView;
import com.android.systemui.notetask.NoteTaskController;
@@ -191,7 +192,6 @@ import com.android.systemui.statusbar.LiftReveal;
import com.android.systemui.statusbar.LightRevealScrim;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 74b1c3bbfd77..2c8866fef030 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -38,6 +38,7 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.InitController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.power.domain.interactor.PowerInteractor;
@@ -50,7 +51,6 @@ import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 29528502aa03..db1977b3ff45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -45,6 +45,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.Mobil
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryKairosImpl
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosAdapter
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosImpl
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
@@ -92,8 +93,10 @@ import kotlinx.coroutines.flow.Flow
DemoModeMobileConnectionDataSourceKairosImpl.Module::class,
MobileRepositorySwitcherKairos.Module::class,
MobileConnectionsRepositoryKairosImpl.Module::class,
+ MobileIconsInteractorKairosImpl.Module::class,
MobileConnectionRepositoryKairosFactoryImpl.Module::class,
MobileConnectionsRepositoryKairosAdapter.Module::class,
+ MobileIconsInteractorKairosAdapter.Module::class,
]
)
abstract class StatusBarPipelineModule {
@@ -171,7 +174,7 @@ abstract class StatusBarPipelineModule {
@Provides
fun mobileIconsInteractor(
impl: Provider<MobileIconsInteractorImpl>,
- kairosImpl: Provider<MobileIconsInteractorKairosImpl>,
+ kairosImpl: Provider<MobileIconsInteractorKairosAdapter>,
): MobileIconsInteractor {
return if (Flags.statusBarMobileIconKairos()) {
kairosImpl.get()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt
index 4580ad974b29..a9399593973b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,38 +22,37 @@ import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.KairosBuilder
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairos.map
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+@ExperimentalKairosApi
interface MobileIconInteractorKairos {
/** The table log created for this connection */
val tableLogBuffer: TableLogBuffer
/** The current mobile data activity */
- val activity: Flow<DataActivityModel>
+ val activity: State<DataActivityModel>
/** See [MobileConnectionsRepository.mobileIsDefault]. */
- val mobileIsDefault: Flow<Boolean>
+ val mobileIsDefault: State<Boolean>
/**
* True when telephony tells us that the data state is CONNECTED. See
@@ -61,31 +60,31 @@ interface MobileIconInteractorKairos {
* consider this connection to be serving data, and thus want to show a network type icon, when
* data is connected. Other data connection states would typically cause us not to show the icon
*/
- val isDataConnected: StateFlow<Boolean>
+ val isDataConnected: State<Boolean>
/** True if we consider this connection to be in service, i.e. can make calls */
- val isInService: StateFlow<Boolean>
+ val isInService: State<Boolean>
/** True if this connection is emergency only */
- val isEmergencyOnly: StateFlow<Boolean>
+ val isEmergencyOnly: State<Boolean>
/** Observable for the data enabled state of this connection */
- val isDataEnabled: StateFlow<Boolean>
+ val isDataEnabled: State<Boolean>
/** True if the RAT icon should always be displayed and false otherwise. */
- val alwaysShowDataRatIcon: StateFlow<Boolean>
+ val alwaysShowDataRatIcon: State<Boolean>
/** Canonical representation of the current mobile signal strength as a triangle. */
- val signalLevelIcon: StateFlow<SignalIconModel>
+ val signalLevelIcon: State<SignalIconModel>
/** Observable for RAT type (network type) indicator */
- val networkTypeIconGroup: StateFlow<NetworkTypeIconModel>
+ val networkTypeIconGroup: State<NetworkTypeIconModel>
/** Whether or not to show the slice attribution */
- val showSliceAttribution: StateFlow<Boolean>
+ val showSliceAttribution: State<Boolean>
/** True if this connection is satellite-based */
- val isNonTerrestrial: StateFlow<Boolean>
+ val isNonTerrestrial: State<Boolean>
/**
* Provider name for this network connection. The name can be one of 3 values:
@@ -95,7 +94,7 @@ interface MobileIconInteractorKairos {
* override in [connectionInfo.operatorAlphaShort], a value that is derived from
* [ServiceState]
*/
- val networkName: StateFlow<NetworkNameModel>
+ val networkName: State<NetworkNameModel>
/**
* Provider name for this network connection. The name can be one of 3 values:
@@ -108,119 +107,110 @@ interface MobileIconInteractorKairos {
* TODO(b/296600321): De-duplicate this field with [networkName] after determining the data
* provided is identical
*/
- val carrierName: StateFlow<String>
+ val carrierName: State<String>
/** True if there is only one active subscription. */
- val isSingleCarrier: StateFlow<Boolean>
+ val isSingleCarrier: State<Boolean>
/**
* True if this connection is considered roaming. The roaming bit can come from [ServiceState],
* or directly from the telephony manager's CDMA ERI number value. Note that we don't consider a
* connection to be roaming while carrier network change is active
*/
- val isRoaming: StateFlow<Boolean>
+ val isRoaming: State<Boolean>
/** See [MobileIconsInteractor.isForceHidden]. */
- val isForceHidden: Flow<Boolean>
+ val isForceHidden: State<Boolean>
/** See [MobileConnectionRepository.isAllowedDuringAirplaneMode]. */
- val isAllowedDuringAirplaneMode: StateFlow<Boolean>
+ val isAllowedDuringAirplaneMode: State<Boolean>
/** True when in carrier network change mode */
- val carrierNetworkChangeActive: StateFlow<Boolean>
+ val carrierNetworkChangeActive: State<Boolean>
}
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@ExperimentalKairosApi
class MobileIconInteractorKairosImpl(
- @Background scope: CoroutineScope,
- defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
- override val alwaysShowDataRatIcon: StateFlow<Boolean>,
- alwaysUseCdmaLevel: StateFlow<Boolean>,
- override val isSingleCarrier: StateFlow<Boolean>,
- override val mobileIsDefault: StateFlow<Boolean>,
- defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
- defaultMobileIconGroup: StateFlow<MobileIconGroup>,
- isDefaultConnectionFailed: StateFlow<Boolean>,
- override val isForceHidden: Flow<Boolean>,
- connectionRepository: MobileConnectionRepository,
+ defaultSubscriptionHasDataEnabled: State<Boolean>,
+ override val alwaysShowDataRatIcon: State<Boolean>,
+ alwaysUseCdmaLevel: State<Boolean>,
+ override val isSingleCarrier: State<Boolean>,
+ override val mobileIsDefault: State<Boolean>,
+ defaultMobileIconMapping: State<Map<String, MobileIconGroup>>,
+ defaultMobileIconGroup: State<MobileIconGroup>,
+ isDefaultConnectionFailed: State<Boolean>,
+ override val isForceHidden: State<Boolean>,
+ private val connectionRepository: MobileConnectionRepositoryKairos,
private val context: Context,
- val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl(),
-) : MobileIconInteractor, MobileIconInteractorKairos {
- override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer
+ private val carrierIdOverrides: MobileIconCarrierIdOverrides =
+ MobileIconCarrierIdOverridesImpl(),
+) : MobileIconInteractorKairos, KairosBuilder by kairosBuilder() {
+ override val tableLogBuffer: TableLogBuffer
+ get() = connectionRepository.tableLogBuffer
- override val activity = connectionRepository.dataActivityDirection
+ override val activity: State<DataActivityModel>
+ get() = connectionRepository.dataActivityDirection
- override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
+ override val isDataEnabled: State<Boolean> = connectionRepository.dataEnabled
- override val carrierNetworkChangeActive: StateFlow<Boolean> =
- connectionRepository.carrierNetworkChangeActive
+ override val carrierNetworkChangeActive: State<Boolean>
+ get() = connectionRepository.carrierNetworkChangeActive
// True if there exists _any_ icon override for this carrierId. Note that overrides can include
// any or none of the icon groups defined in MobileMappings, so we still need to check on a
// per-network-type basis whether or not the given icon group is overridden
- private val carrierIdIconOverrideExists =
- connectionRepository.carrierId
- .map { carrierIdOverrides.carrierIdEntryExists(it) }
- .distinctUntilChanged()
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ private val carrierIdIconOverrideExists: State<Boolean> =
+ connectionRepository.carrierId.map { carrierIdOverrides.carrierIdEntryExists(it) }
- override val networkName =
+ override val networkName: State<NetworkNameModel> =
combine(connectionRepository.operatorAlphaShort, connectionRepository.networkName) {
- operatorAlphaShort,
- networkName ->
- if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
- NetworkNameModel.IntentDerived(operatorAlphaShort)
- } else {
- networkName
- }
+ operatorAlphaShort,
+ networkName ->
+ if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
+ NetworkNameModel.IntentDerived(operatorAlphaShort)
+ } else {
+ networkName
}
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- connectionRepository.networkName.value,
- )
+ }
- override val carrierName =
+ override val carrierName: State<String> =
combine(connectionRepository.operatorAlphaShort, connectionRepository.carrierName) {
- operatorAlphaShort,
- networkName ->
- if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
- operatorAlphaShort
- } else {
- networkName.name
- }
+ operatorAlphaShort,
+ networkName ->
+ if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
+ operatorAlphaShort
+ } else {
+ networkName.name
}
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- connectionRepository.carrierName.value.name,
- )
+ }
/** What the mobile icon would be before carrierId overrides */
- private val defaultNetworkType: StateFlow<MobileIconGroup> =
+ private val defaultNetworkType: State<MobileIconGroup> =
combine(
- connectionRepository.resolvedNetworkType,
- defaultMobileIconMapping,
- defaultMobileIconGroup,
- ) { resolvedNetworkType, mapping, defaultGroup ->
- when (resolvedNetworkType) {
- is ResolvedNetworkType.CarrierMergedNetworkType ->
- resolvedNetworkType.iconGroupOverride
- else -> {
- mapping[resolvedNetworkType.lookupKey] ?: defaultGroup
- }
+ connectionRepository.resolvedNetworkType,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ ) { resolvedNetworkType, mapping, defaultGroup ->
+ when (resolvedNetworkType) {
+ is ResolvedNetworkType.CarrierMergedNetworkType ->
+ resolvedNetworkType.iconGroupOverride
+
+ else -> {
+ mapping[resolvedNetworkType.lookupKey] ?: defaultGroup
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
+ }
- override val networkTypeIconGroup =
- combine(defaultNetworkType, carrierIdIconOverrideExists) { networkType, overrideExists ->
+ override val networkTypeIconGroup: State<NetworkTypeIconModel> = buildState {
+ combineTransactionally(defaultNetworkType, carrierIdIconOverrideExists) {
+ networkType,
+ overrideExists ->
// DefaultIcon comes out of the icongroup lookup, we check for overrides here
if (overrideExists) {
val iconOverride =
carrierIdOverrides.getOverrideFor(
- connectionRepository.carrierId.value,
+ connectionRepository.carrierId.sample(),
networkType.name,
context.resources,
)
@@ -233,106 +223,101 @@ class MobileIconInteractorKairosImpl(
DefaultIcon(networkType)
}
}
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogBuffer = tableLogBuffer,
- initialValue = DefaultIcon(defaultMobileIconGroup.value),
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- DefaultIcon(defaultMobileIconGroup.value),
- )
+ .also { logDiffsForTable(it, tableLogBuffer = tableLogBuffer) }
+ }
- override val showSliceAttribution: StateFlow<Boolean> =
+ override val showSliceAttribution: State<Boolean> =
combine(
- connectionRepository.allowNetworkSliceIndicator,
- connectionRepository.hasPrioritizedNetworkCapabilities,
- ) { allowed, hasPrioritizedNetworkCapabilities ->
- allowed && hasPrioritizedNetworkCapabilities
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ connectionRepository.allowNetworkSliceIndicator,
+ connectionRepository.hasPrioritizedNetworkCapabilities,
+ ) { allowed, hasPrioritizedNetworkCapabilities ->
+ allowed && hasPrioritizedNetworkCapabilities
+ }
- override val isNonTerrestrial: StateFlow<Boolean> = connectionRepository.isNonTerrestrial
+ override val isNonTerrestrial: State<Boolean>
+ get() = connectionRepository.isNonTerrestrial
- override val isRoaming: StateFlow<Boolean> =
+ override val isRoaming: State<Boolean> =
combine(
- connectionRepository.carrierNetworkChangeActive,
- connectionRepository.isGsm,
- connectionRepository.isRoaming,
- connectionRepository.cdmaRoaming,
- ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming ->
- if (carrierNetworkChangeActive) {
- false
- } else if (isGsm) {
- isRoaming
- } else {
- cdmaRoaming
- }
+ connectionRepository.carrierNetworkChangeActive,
+ connectionRepository.isGsm,
+ connectionRepository.isRoaming,
+ connectionRepository.cdmaRoaming,
+ ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming ->
+ if (carrierNetworkChangeActive) {
+ false
+ } else if (isGsm) {
+ isRoaming
+ } else {
+ cdmaRoaming
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ }
- private val level: StateFlow<Int> =
+ private val level: State<Int> =
combine(
- connectionRepository.isGsm,
- connectionRepository.primaryLevel,
- connectionRepository.cdmaLevel,
- alwaysUseCdmaLevel,
- ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel ->
- when {
- // GSM connections should never use the CDMA level
- isGsm -> primaryLevel
- alwaysUseCdmaLevel -> cdmaLevel
- else -> primaryLevel
- }
+ connectionRepository.isGsm,
+ connectionRepository.primaryLevel,
+ connectionRepository.cdmaLevel,
+ alwaysUseCdmaLevel,
+ ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel ->
+ when {
+ // GSM connections should never use the CDMA level
+ isGsm -> primaryLevel
+ alwaysUseCdmaLevel -> cdmaLevel
+ else -> primaryLevel
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+ }
- private val numberOfLevels: StateFlow<Int> = connectionRepository.numberOfLevels
+ private val numberOfLevels: State<Int>
+ get() = connectionRepository.numberOfLevels
- override val isDataConnected: StateFlow<Boolean> =
+ override val isDataConnected: State<Boolean> =
connectionRepository.dataConnectionState
.map { it == Connected }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, "icon", "isDataConnected") }
+ }
- override val isInService = connectionRepository.isInService
+ override val isInService
+ get() = connectionRepository.isInService
- override val isEmergencyOnly: StateFlow<Boolean> = connectionRepository.isEmergencyOnly
+ override val isEmergencyOnly: State<Boolean>
+ get() = connectionRepository.isEmergencyOnly
- override val isAllowedDuringAirplaneMode = connectionRepository.isAllowedDuringAirplaneMode
+ override val isAllowedDuringAirplaneMode: State<Boolean>
+ get() = connectionRepository.isAllowedDuringAirplaneMode
/** Whether or not to show the error state of [SignalDrawable] */
- private val showExclamationMark: StateFlow<Boolean> =
+ private val showExclamationMark: State<Boolean> =
combine(defaultSubscriptionHasDataEnabled, isDefaultConnectionFailed, isInService) {
- isDefaultDataEnabled,
- isDefaultConnectionFailed,
- isInService ->
- !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), true)
+ isDefaultDataEnabled,
+ isDefaultConnectionFailed,
+ isInService ->
+ !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService
+ }
- private val cellularShownLevel: StateFlow<Int> =
+ private val cellularShownLevel: State<Int> =
combine(level, isInService, connectionRepository.inflateSignalStrength) {
- level,
- isInService,
- inflate ->
- if (isInService) {
- if (inflate) level + 1 else level
- } else 0
+ level,
+ isInService,
+ inflate ->
+ when {
+ !isInService -> 0
+ inflate -> level + 1
+ else -> level
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+ }
// Satellite level is unaffected by the inflateSignalStrength property
// See b/346904529 for details
- private val satelliteShownLevel: StateFlow<Int> =
+ private val satelliteShownLevel: State<Int> =
if (Flags.carrierRoamingNbIotNtn()) {
- connectionRepository.satelliteLevel
- } else {
- combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+ connectionRepository.satelliteLevel
+ } else {
+ combine(level, isInService) { level, isInService -> if (isInService) level else 0 }
+ }
- private val cellularIcon: Flow<SignalIconModel.Cellular> =
+ private val cellularIcon: State<SignalIconModel.Cellular> =
combine(
cellularShownLevel,
numberOfLevels,
@@ -347,7 +332,7 @@ class MobileIconInteractorKairosImpl(
)
}
- private val satelliteIcon: Flow<SignalIconModel.Satellite> =
+ private val satelliteIcon: State<SignalIconModel.Satellite> =
satelliteShownLevel.map {
SignalIconModel.Satellite(
level = it,
@@ -357,24 +342,14 @@ class MobileIconInteractorKairosImpl(
)
}
- override val signalLevelIcon: StateFlow<SignalIconModel> = run {
- val initial =
- SignalIconModel.Cellular(
- cellularShownLevel.value,
- numberOfLevels.value,
- showExclamationMark.value,
- carrierNetworkChangeActive.value,
- )
+ override val signalLevelIcon: State<SignalIconModel> =
isNonTerrestrial
- .flatMapLatest { ntn ->
+ .flatMap { ntn ->
if (ntn) {
satelliteIcon
} else {
cellularIcon
}
}
- .distinctUntilChanged()
- .logDiffsForTable(tableLogBuffer, columnPrefix = "icon", initialValue = initial)
- .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
- }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "icon") } }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt
new file mode 100644
index 000000000000..a3b9b17257cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.toColdConflatedFlow
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+@ExperimentalKairosApi
+fun BuildScope.MobileIconInteractorKairosAdapter(
+ kairosImpl: MobileIconInteractorKairos
+): MobileIconInteractor =
+ with(kairosImpl) {
+ MobileIconInteractorKairosAdapter(
+ tableLogBuffer = tableLogBuffer,
+ activity = activity.toColdConflatedFlow(kairosNetwork),
+ mobileIsDefault = mobileIsDefault.toColdConflatedFlow(kairosNetwork),
+ isDataConnected = isDataConnected.toStateFlow(),
+ isInService = isInService.toStateFlow(),
+ isEmergencyOnly = isEmergencyOnly.toStateFlow(),
+ isDataEnabled = isDataEnabled.toStateFlow(),
+ alwaysShowDataRatIcon = alwaysShowDataRatIcon.toStateFlow(),
+ signalLevelIcon = signalLevelIcon.toStateFlow(),
+ networkTypeIconGroup = networkTypeIconGroup.toStateFlow(),
+ showSliceAttribution = showSliceAttribution.toStateFlow(),
+ isNonTerrestrial = isNonTerrestrial.toStateFlow(),
+ networkName = networkName.toStateFlow(),
+ carrierName = carrierName.toStateFlow(),
+ isSingleCarrier = isSingleCarrier.toStateFlow(),
+ isRoaming = isRoaming.toStateFlow(),
+ isForceHidden = isForceHidden.toColdConflatedFlow(kairosNetwork),
+ isAllowedDuringAirplaneMode = isAllowedDuringAirplaneMode.toStateFlow(),
+ carrierNetworkChangeActive = carrierNetworkChangeActive.toStateFlow(),
+ )
+ }
+
+private class MobileIconInteractorKairosAdapter(
+ override val tableLogBuffer: TableLogBuffer,
+ override val activity: Flow<DataActivityModel>,
+ override val mobileIsDefault: Flow<Boolean>,
+ override val isDataConnected: StateFlow<Boolean>,
+ override val isInService: StateFlow<Boolean>,
+ override val isEmergencyOnly: StateFlow<Boolean>,
+ override val isDataEnabled: StateFlow<Boolean>,
+ override val alwaysShowDataRatIcon: StateFlow<Boolean>,
+ override val signalLevelIcon: StateFlow<SignalIconModel>,
+ override val networkTypeIconGroup: StateFlow<NetworkTypeIconModel>,
+ override val showSliceAttribution: StateFlow<Boolean>,
+ override val isNonTerrestrial: StateFlow<Boolean>,
+ override val networkName: StateFlow<NetworkNameModel>,
+ override val carrierName: StateFlow<String>,
+ override val isSingleCarrier: StateFlow<Boolean>,
+ override val isRoaming: StateFlow<Boolean>,
+ override val isForceHidden: Flow<Boolean>,
+ override val isAllowedDuringAirplaneMode: StateFlow<Boolean>,
+ override val carrierNetworkChangeActive: StateFlow<Boolean>,
+) : MobileIconInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt
index e8e0a833af2a..14a276b60933 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,41 +21,47 @@ import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING
import com.android.settingslib.SignalIcon.MobileIconGroup
-import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.activated
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.Incremental
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.asyncEvent
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.filter
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairos.flatten
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.core.NewStatusBarIcons
import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
import com.android.systemui.util.CarrierConfigTracker
-import java.lang.ref.WeakReference
+import dagger.Binds
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import javax.inject.Provider
+import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
/**
* Business layer logic for the set of mobile subscription icons.
@@ -67,98 +73,79 @@ import kotlinx.coroutines.flow.transformLatest
* represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
* icon
*/
+@ExperimentalKairosApi
interface MobileIconsInteractorKairos {
/** See [MobileConnectionsRepository.mobileIsDefault]. */
- val mobileIsDefault: StateFlow<Boolean>
+ val mobileIsDefault: State<Boolean>
/** List of subscriptions, potentially filtered for CBRS */
- val filteredSubscriptions: Flow<List<SubscriptionModel>>
-
- /** Subscription ID of the current default data subscription */
- val defaultDataSubId: Flow<Int?>
+ val filteredSubscriptions: State<List<SubscriptionModel>>
/**
* The current list of [MobileIconInteractor]s associated with the current list of
* [filteredSubscriptions]
*/
- val icons: StateFlow<List<MobileIconInteractor>>
+ val icons: Incremental<Int, MobileIconInteractorKairos>
/** Whether the mobile icons can be stacked vertically. */
- val isStackable: StateFlow<Boolean>
-
- /**
- * Observable for the subscriptionId of the current mobile data connection. Null if we don't
- * have a valid subscription id
- */
- val activeMobileDataSubscriptionId: StateFlow<Int?>
+ val isStackable: State<Boolean>
/** True if the active mobile data subscription has data enabled */
- val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
+ val activeDataConnectionHasDataEnabled: State<Boolean>
/**
* Flow providing a reference to the Interactor for the active data subId. This represents the
- * [MobileIconInteractor] responsible for the active data connection, if any.
+ * [MobileIconInteractorKairos] responsible for the active data connection, if any.
*/
- val activeDataIconInteractor: StateFlow<MobileIconInteractor?>
+ val activeDataIconInteractor: State<MobileIconInteractorKairos?>
/** True if the RAT icon should always be displayed and false otherwise. */
- val alwaysShowDataRatIcon: StateFlow<Boolean>
+ val alwaysShowDataRatIcon: State<Boolean>
/** True if the CDMA level should be preferred over the primary level. */
- val alwaysUseCdmaLevel: StateFlow<Boolean>
+ val alwaysUseCdmaLevel: State<Boolean>
/** True if there is only one active subscription. */
- val isSingleCarrier: StateFlow<Boolean>
+ val isSingleCarrier: State<Boolean>
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
- val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
+ val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
- val defaultMobileIconGroup: StateFlow<MobileIconGroup>
+ val defaultMobileIconGroup: State<MobileIconGroup>
/** True only if the default network is mobile, and validation also failed */
- val isDefaultConnectionFailed: StateFlow<Boolean>
+ val isDefaultConnectionFailed: State<Boolean>
/** True once the user has been set up */
- val isUserSetUp: StateFlow<Boolean>
+ val isUserSetUp: State<Boolean>
/** True if we're configured to force-hide the mobile icons and false otherwise. */
- val isForceHidden: Flow<Boolean>
+ val isForceHidden: State<Boolean>
/**
* True if the device-level service state (with -1 subscription id) reports emergency calls
* only. This value is only useful when there are no other subscriptions OR all existing
* subscriptions report that they are not in service.
*/
- val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean>
-
- /**
- * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
- * subId.
- */
- fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
+ val isDeviceInEmergencyCallsOnlyMode: State<Boolean>
}
-@OptIn(ExperimentalCoroutinesApi::class)
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@ExperimentalKairosApi
@SysUISingleton
class MobileIconsInteractorKairosImpl
@Inject
constructor(
- private val mobileConnectionsRepo: MobileConnectionsRepository,
+ private val mobileConnectionsRepo: MobileConnectionsRepositoryKairos,
private val carrierConfigTracker: CarrierConfigTracker,
@MobileSummaryLog private val tableLogger: TableLogBuffer,
connectivityRepository: ConnectivityRepository,
userSetupRepo: UserSetupRepository,
- @Background private val scope: CoroutineScope,
private val context: Context,
private val featureFlagsClassic: FeatureFlagsClassic,
-) : MobileIconsInteractor, MobileIconsInteractorKairos {
-
- // Weak reference lookup for created interactors
- private val reuseCache = mutableMapOf<Int, WeakReference<MobileIconInteractor>>()
+) : MobileIconsInteractorKairos, KairosBuilder by kairosBuilder() {
- override val mobileIsDefault =
+ override val mobileIsDefault: State<Boolean> =
combine(
mobileConnectionsRepo.mobileIsDefault,
mobileConnectionsRepo.hasCarrierMergedConnection,
@@ -167,47 +154,36 @@ constructor(
// the `isDefault` calculation. See b/272586234.
mobileIsDefault || hasCarrierMergedConnection
}
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "mobileIsDefault",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val activeMobileDataSubscriptionId: StateFlow<Int?> =
- mobileConnectionsRepo.activeMobileDataSubscriptionId
-
- override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
- mobileConnectionsRepo.activeMobileDataRepository
- .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> =
- mobileConnectionsRepo.activeMobileDataSubscriptionId
- .mapLatest {
- if (it != null) {
- getMobileConnectionInteractorForSubId(it)
- } else {
- null
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "mobileIsDefault",
+ )
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), null)
- private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
- mobileConnectionsRepo.subscriptions
+ override val activeDataConnectionHasDataEnabled: State<Boolean> =
+ mobileConnectionsRepo.activeMobileDataRepository.flatMap {
+ it?.dataEnabled ?: stateOf(false)
+ }
+
+ private val unfilteredSubscriptions: State<Collection<SubscriptionModel>>
+ get() = mobileConnectionsRepo.subscriptions
/** Any filtering that we can do based purely on the info of each subscription individually. */
- private val subscriptionsBasedFilteredSubs =
- unfilteredSubscriptions
- .map { it.filterBasedOnProvisioning().filterBasedOnNtn() }
- .distinctUntilChanged()
+ private val subscriptionsBasedFilteredSubs: State<List<SubscriptionModel>> =
+ unfilteredSubscriptions.map {
+ it.asSequence().filterBasedOnProvisioning().filterBasedOnNtn().toList()
+ }
- private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> =
+ private fun Sequence<SubscriptionModel>.filterBasedOnProvisioning() =
if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
this
} else {
- this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
+ filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
}
/**
@@ -219,9 +195,10 @@ constructor(
* need to filter out those subscriptions here so we guarantee the subscription never turns into
* an icon. See b/336881301.
*/
- private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> {
- return this.filter { !it.isExclusivelyNonTerrestrial }
- }
+ private fun Sequence<SubscriptionModel>.filterBasedOnNtn(): Sequence<SubscriptionModel> =
+ filter {
+ !it.isExclusivelyNonTerrestrial
+ }
/**
* Generally, SystemUI wants to show iconography for each subscription that is listed by
@@ -236,22 +213,23 @@ constructor(
* [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
* and by checking which subscription is opportunistic, or which one is active.
*/
- override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
+ override val filteredSubscriptions: State<List<SubscriptionModel>> = buildState {
combine(
subscriptionsBasedFilteredSubs,
mobileConnectionsRepo.activeMobileDataSubscriptionId,
- connectivityRepository.vcnSubId,
+ connectivityRepository.vcnSubId.toState(),
) { preFilteredSubs, activeId, vcnSubId ->
filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
}
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "filteredSubscriptions",
- initialValue = listOf(),
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+ .also {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "filteredSubscriptions",
+ )
+ }
+ }
private fun filterSubsBasedOnOpportunistic(
subList: List<SubscriptionModel>,
@@ -298,19 +276,25 @@ constructor(
}
}
- override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
-
- override val icons =
- filteredSubscriptions
- .mapLatest { subs ->
- subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) }
+ override val icons: Incremental<Int, MobileIconInteractorKairos> = buildIncremental {
+ val filteredSubIds =
+ filteredSubscriptions.map { it.asSequence().map { sub -> sub.subscriptionId }.toSet() }
+ mobileConnectionsRepo.mobileConnectionsBySubId
+ .filterIncrementally { (subId, _) ->
+ // Filter out repo if subId is not present in the filtered set
+ filteredSubIds.map { subId in it }
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+ // Just map the repos to interactors
+ .mapValues { (subId, repo) -> buildSpec { mobileConnection(repo) } }
+ .applyLatestSpecForKey()
+ }
- override val isStackable =
+ override val isStackable: State<Boolean> =
if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) {
- icons.flatMapLatest { icons ->
- combine(icons.map { it.signalLevelIcon }) { signalLevelIcons ->
+ icons.flatMap { iconsBySubId: Map<Int, MobileIconInteractorKairos> ->
+ iconsBySubId.values
+ .map { it.signalLevelIcon }
+ .combine { signalLevelIcons ->
// These are only stackable if:
// - They are cellular
// - There's exactly two
@@ -319,11 +303,15 @@ constructor(
it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels
}
}
- }
- } else {
- flowOf(false)
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ } else {
+ stateOf(false)
+ }
+
+ override val activeDataIconInteractor: State<MobileIconInteractorKairos?> =
+ combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons ->
+ activeSubId?.let { icons[activeSubId] }
+ }
/**
* Copied from the old pipeline. We maintain a 2s period of time where we will keep the
@@ -335,67 +323,59 @@ constructor(
*
* The goal of this is to minimize the flickering in the UI of the cellular indicator
*/
- private val forcingCellularValidation =
+ private val forcingCellularValidation: State<Boolean> = buildState {
mobileConnectionsRepo.activeSubChangedInGroupEvent
- .filter { mobileConnectionsRepo.defaultConnectionIsValidated.value }
- .transformLatest {
- emit(true)
- delay(2000)
- emit(false)
+ .filter(mobileConnectionsRepo.defaultConnectionIsValidated)
+ .mapLatestBuild {
+ asyncEvent {
+ delay(2.seconds)
+ false
+ }
+ .holdState(true)
+ }
+ .holdState(stateOf(false))
+ .flatten()
+ .also {
+ logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "forcingValidation")
}
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "forcingValidation",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ }
/**
* Mapping from network type to [MobileIconGroup] using the config generated for the default
* subscription Id. This mapping is the same for every subscription.
*/
- override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
- mobileConnectionsRepo.defaultMobileIconMapping.stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- initialValue = mapOf(),
- )
+ override val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
+ get() = mobileConnectionsRepo.defaultMobileIconMapping
- override val alwaysShowDataRatIcon: StateFlow<Boolean> =
- mobileConnectionsRepo.defaultDataSubRatConfig
- .mapLatest { it.alwaysShowDataRatIcon }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ override val alwaysShowDataRatIcon: State<Boolean> =
+ mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowDataRatIcon }
- override val alwaysUseCdmaLevel: StateFlow<Boolean> =
- mobileConnectionsRepo.defaultDataSubRatConfig
- .mapLatest { it.alwaysShowCdmaRssi }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ override val alwaysUseCdmaLevel: State<Boolean> =
+ mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowCdmaRssi }
- override val isSingleCarrier: StateFlow<Boolean> =
+ override val isSingleCarrier: State<Boolean> =
mobileConnectionsRepo.subscriptions
.map { it.size == 1 }
- .logDiffsForTable(
- tableLogger,
- columnPrefix = LOGGING_PREFIX,
- columnName = "isSingleCarrier",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ columnPrefix = LOGGING_PREFIX,
+ columnName = "isSingleCarrier",
+ )
+ }
+ }
/** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
- override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
- mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- initialValue = TelephonyIcons.G,
- )
+ override val defaultMobileIconGroup: State<MobileIconGroup>
+ get() = mobileConnectionsRepo.defaultMobileIconGroup
/**
* We want to show an error state when cellular has actually failed to validate, but not if some
* other transport type is active, because then we expect there not to be validation.
*/
- override val isDefaultConnectionFailed: StateFlow<Boolean> =
+ override val isDefaultConnectionFailed: State<Boolean> =
combine(
mobileIsDefault,
mobileConnectionsRepo.defaultConnectionIsValidated,
@@ -407,46 +387,63 @@ constructor(
else -> !defaultConnectionIsValidated
}
}
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "isDefaultConnectionFailed",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val isUserSetUp: StateFlow<Boolean> = userSetupRepo.isUserSetUp
-
- override val isForceHidden: Flow<Boolean> =
- connectivityRepository.forceHiddenSlots
- .map { it.contains(ConnectivitySlot.MOBILE) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> =
- mobileConnectionsRepo.isDeviceEmergencyCallCapable
-
- /** Vends out new [MobileIconInteractor] for a particular subId */
- override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
- reuseCache[subId]?.get() ?: createMobileConnectionInteractorForSubId(subId)
-
- private fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
- MobileIconInteractorImpl(
- scope,
- activeDataConnectionHasDataEnabled,
- alwaysShowDataRatIcon,
- alwaysUseCdmaLevel,
- isSingleCarrier,
- mobileIsDefault,
- defaultMobileIconMapping,
- defaultMobileIconGroup,
- isDefaultConnectionFailed,
- isForceHidden,
- mobileConnectionsRepo.getRepoForSubId(subId),
- context,
- )
- .also { reuseCache[subId] = WeakReference(it) }
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "isDefaultConnectionFailed",
+ )
+ }
+ }
+
+ override val isUserSetUp: State<Boolean> = buildState { userSetupRepo.isUserSetUp.toState() }
+
+ override val isForceHidden: State<Boolean> = buildState {
+ connectivityRepository.forceHiddenSlots.toState().map {
+ it.contains(ConnectivitySlot.MOBILE)
+ }
+ }
+
+ override val isDeviceInEmergencyCallsOnlyMode: State<Boolean>
+ get() = mobileConnectionsRepo.isDeviceEmergencyCallCapable
+
+ /** Vends out a new [MobileIconInteractorKairos] for a particular subId */
+ private fun BuildScope.mobileConnection(
+ repo: MobileConnectionRepositoryKairos
+ ): MobileIconInteractorKairos = activated {
+ MobileIconInteractorKairosImpl(
+ activeDataConnectionHasDataEnabled,
+ alwaysShowDataRatIcon,
+ alwaysUseCdmaLevel,
+ isSingleCarrier,
+ mobileIsDefault,
+ defaultMobileIconMapping,
+ defaultMobileIconGroup,
+ isDefaultConnectionFailed,
+ isForceHidden,
+ repo,
+ context,
+ )
+ }
companion object {
private const val LOGGING_PREFIX = "Intr"
}
+
+ @dagger.Module
+ interface Module {
+
+ @Binds fun bindImpl(impl: MobileIconsInteractorKairosImpl): MobileIconsInteractorKairos
+
+ companion object {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileIconsInteractorKairosImpl>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt
new file mode 100644
index 000000000000..87877b3e9f43
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.content.Context
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.toColdConflatedFlow
+import com.android.systemui.kairosBuilder
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+@ExperimentalKairosApi
+@SysUISingleton
+class MobileIconsInteractorKairosAdapter
+@Inject
+constructor(
+ private val kairosInteractor: MobileIconsInteractorKairos,
+ private val repo: MobileConnectionsRepository,
+ repoK: MobileConnectionsRepositoryKairos,
+ kairosNetwork: KairosNetwork,
+ @Application scope: CoroutineScope,
+ context: Context,
+ mobileMappingsProxy: MobileMappingsProxy,
+ private val userSetupRepo: UserSetupRepository,
+) : MobileIconsInteractor, KairosBuilder by kairosBuilder() {
+
+ private val interactorsBySubIdK = buildIncremental {
+ kairosInteractor.icons
+ .mapValues { (subId, interactor) ->
+ buildSpec { MobileIconInteractorKairosAdapter(interactor) }
+ }
+ .applyLatestSpecForKey()
+ }
+
+ private val interactorsBySubId =
+ interactorsBySubIdK
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.Eagerly, emptyMap())
+
+ override val defaultDataSubId: Flow<Int?>
+ get() = repo.defaultDataSubId
+
+ override val mobileIsDefault: StateFlow<Boolean> =
+ kairosInteractor.mobileIsDefault
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), repo.mobileIsDefault.value)
+
+ override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
+ kairosInteractor.filteredSubscriptions.toColdConflatedFlow(kairosNetwork)
+
+ override val icons: StateFlow<List<MobileIconInteractor>> =
+ interactorsBySubIdK
+ .map { it.values.toList() }
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+ override val isStackable: StateFlow<Boolean> =
+ kairosInteractor.isStackable
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activeMobileDataSubscriptionId: StateFlow<Int?>
+ get() = repo.activeMobileDataSubscriptionId
+
+ override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
+ kairosInteractor.activeDataConnectionHasDataEnabled
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> =
+ combine(repoK.activeMobileDataSubscriptionId, interactorsBySubIdK) { subId, interactors ->
+ interactors[subId]
+ }
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val alwaysShowDataRatIcon: StateFlow<Boolean> =
+ kairosInteractor.alwaysShowDataRatIcon
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val alwaysUseCdmaLevel: StateFlow<Boolean> =
+ kairosInteractor.alwaysUseCdmaLevel
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isSingleCarrier: StateFlow<Boolean> =
+ kairosInteractor.isSingleCarrier
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val defaultMobileIconMapping: StateFlow<Map<String, SignalIcon.MobileIconGroup>> =
+ kairosInteractor.defaultMobileIconMapping
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyMap())
+
+ override val defaultMobileIconGroup: StateFlow<SignalIcon.MobileIconGroup> =
+ kairosInteractor.defaultMobileIconGroup
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ mobileMappingsProxy.getDefaultIcons(MobileMappings.Config.readConfig(context)),
+ )
+
+ override val isDefaultConnectionFailed: StateFlow<Boolean> =
+ kairosInteractor.isDefaultConnectionFailed
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isUserSetUp: StateFlow<Boolean>
+ get() = userSetupRepo.isUserSetUp
+
+ override val isForceHidden: Flow<Boolean> =
+ kairosInteractor.isForceHidden
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean>
+ get() = repo.isDeviceEmergencyCallCapable
+
+ override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+ interactorsBySubId.value[subId] ?: error("Unknown subscription id: $subId")
+
+ @dagger.Module
+ object Module {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileIconsInteractorKairosAdapter>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt
new file mode 100644
index 000000000000..fce8c85338f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import android.graphics.Color
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This
+ * allows the mobile icon to change some view parameters at different locations
+ *
+ * @param commonImpl for convenience, this class wraps a base interface that can provides all of the
+ * common implementations between locations. See [MobileIconViewModel]
+ * @property location the [StatusBarLocation] of this VM.
+ * @property verboseLogger an optional logger to log extremely verbose view updates.
+ */
+abstract class LocationBasedMobileViewModelKairos(
+ val commonImpl: MobileIconViewModelCommonKairos,
+ val location: StatusBarLocation,
+ val verboseLogger: VerboseMobileViewLogger?,
+) : MobileIconViewModelCommonKairos by commonImpl {
+ val defaultColor: Int = Color.WHITE
+
+ companion object {
+ fun viewModelForLocation(
+ commonImpl: MobileIconViewModelCommon,
+ interactor: MobileIconInteractor,
+ verboseMobileViewLogger: VerboseMobileViewLogger,
+ location: StatusBarLocation,
+ scope: CoroutineScope,
+ ): LocationBasedMobileViewModel =
+ when (location) {
+ StatusBarLocation.HOME ->
+ HomeMobileIconViewModel(commonImpl, verboseMobileViewLogger)
+ StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl)
+ StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl)
+ StatusBarLocation.SHADE_CARRIER_GROUP ->
+ ShadeCarrierGroupMobileIconViewModel(commonImpl, interactor, scope)
+ }
+ }
+}
+
+class HomeMobileIconViewModelKairos(
+ commonImpl: MobileIconViewModelCommonKairos,
+ verboseMobileViewLogger: VerboseMobileViewLogger,
+) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.HOME,
+ verboseMobileViewLogger,
+ )
+
+class QsMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.QS,
+ // Only do verbose logging for the Home location.
+ verboseLogger = null,
+ )
+
+class ShadeCarrierGroupMobileIconViewModelKairos(
+ commonImpl: MobileIconViewModelCommonKairos,
+ interactor: MobileIconInteractor,
+ scope: CoroutineScope,
+) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.SHADE_CARRIER_GROUP,
+ // Only do verbose logging for the Home location.
+ verboseLogger = null,
+ ) {
+ private val isSingleCarrier = interactor.isSingleCarrier
+ val carrierName = interactor.carrierName
+
+ override val isVisible: StateFlow<Boolean> =
+ combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier ->
+ if (isSingleCarrier) false else isVisible
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value)
+}
+
+class KeyguardMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) :
+ MobileIconViewModelCommonKairos,
+ LocationBasedMobileViewModelKairos(
+ commonImpl,
+ location = StatusBarLocation.KEYGUARD,
+ // Only do verbose logging for the Home location.
+ verboseLogger = null,
+ )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
new file mode 100644
index 000000000000..cc7fc0964dae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import com.android.systemui.Flags.statusBarStaticInoutIndicators
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/** Common interface for all of the location-based mobile icon view models. */
+interface MobileIconViewModelCommonKairos : MobileIconViewModelCommon {
+ override val subscriptionId: Int
+ /** True if this view should be visible at all. */
+ override val isVisible: StateFlow<Boolean>
+ override val icon: Flow<SignalIconModel>
+ override val contentDescription: Flow<MobileContentDescription?>
+ override val roaming: Flow<Boolean>
+ /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+ override val networkTypeIcon: Flow<Icon.Resource?>
+ /** The slice attribution. Drawn as a background layer */
+ override val networkTypeBackground: StateFlow<Icon.Resource?>
+ override val activityInVisible: Flow<Boolean>
+ override val activityOutVisible: Flow<Boolean>
+ override val activityContainerVisible: Flow<Boolean>
+}
+
+/**
+ * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
+ * a single line of service via [MobileIconInteractor] and update the UI based on that
+ * subscription's information.
+ *
+ * There will be exactly one [MobileIconViewModel] per filtered subscription offered from
+ * [MobileIconsInteractor.filteredSubscriptions].
+ *
+ * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon]
+ * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view
+ * model gets the exact same information, as well as allows us to log that unified state only once
+ * per icon.
+ */
+class MobileIconViewModelKairos(
+ override val subscriptionId: Int,
+ iconInteractor: MobileIconInteractor,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ constants: ConnectivityConstants,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommonKairos {
+ private val cellProvider by lazy {
+ CellularIconViewModelKairos(
+ subscriptionId,
+ iconInteractor,
+ airplaneModeInteractor,
+ constants,
+ scope,
+ )
+ }
+
+ private val satelliteProvider by lazy {
+ CarrierBasedSatelliteViewModelKairosImpl(
+ subscriptionId,
+ airplaneModeInteractor,
+ iconInteractor,
+ scope,
+ )
+ }
+
+ /**
+ * Similar to repository switching, this allows us to split up the logic of satellite/cellular
+ * states, since they are different by nature
+ */
+ private val vmProvider: Flow<MobileIconViewModelCommon> =
+ iconInteractor.isNonTerrestrial
+ .mapLatest { nonTerrestrial ->
+ if (nonTerrestrial) {
+ satelliteProvider
+ } else {
+ cellProvider
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider)
+
+ override val isVisible: StateFlow<Boolean> =
+ vmProvider
+ .flatMapLatest { it.isVisible }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon }
+
+ override val contentDescription: Flow<MobileContentDescription?> =
+ vmProvider.flatMapLatest { it.contentDescription }
+
+ override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming }
+
+ override val networkTypeIcon: Flow<Icon.Resource?> =
+ vmProvider.flatMapLatest { it.networkTypeIcon }
+
+ override val networkTypeBackground: StateFlow<Icon.Resource?> =
+ vmProvider
+ .flatMapLatest { it.networkTypeBackground }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val activityInVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityInVisible }
+
+ override val activityOutVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityOutVisible }
+
+ override val activityContainerVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityContainerVisible }
+}
+
+/** Representation of this network when it is non-terrestrial (e.g., satellite) */
+private class CarrierBasedSatelliteViewModelKairosImpl(
+ override val subscriptionId: Int,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ interactor: MobileIconInteractor,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos {
+ override val isVisible: StateFlow<Boolean> =
+ airplaneModeInteractor.isAirplaneMode
+ .map { !it }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon
+
+ override val contentDescription: Flow<MobileContentDescription?> = MutableStateFlow(null)
+
+ /** These fields are not used for satellite icons currently */
+ override val roaming: Flow<Boolean> = flowOf(false)
+ override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null)
+ override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null)
+ override val activityInVisible: Flow<Boolean> = flowOf(false)
+ override val activityOutVisible: Flow<Boolean> = flowOf(false)
+ override val activityContainerVisible: Flow<Boolean> = flowOf(false)
+}
+
+/** Terrestrial (cellular) icon. */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+private class CellularIconViewModelKairos(
+ override val subscriptionId: Int,
+ iconInteractor: MobileIconInteractor,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ constants: ConnectivityConstants,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos {
+ override val isVisible: StateFlow<Boolean> =
+ if (!constants.hasDataCapabilities) {
+ flowOf(false)
+ } else {
+ combine(
+ airplaneModeInteractor.isAirplaneMode,
+ iconInteractor.isAllowedDuringAirplaneMode,
+ iconInteractor.isForceHidden,
+ ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden ->
+ if (isForceHidden) {
+ false
+ } else if (isAirplaneMode) {
+ isAllowedDuringAirplaneMode
+ } else {
+ true
+ }
+ }
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnName = "visible",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon
+
+ override val contentDescription: Flow<MobileContentDescription?> =
+ combine(iconInteractor.signalLevelIcon, iconInteractor.networkName) { icon, nameModel ->
+ when (icon) {
+ is SignalIconModel.Cellular ->
+ MobileContentDescription.Cellular(
+ nameModel.name,
+ icon.levelDescriptionRes(),
+ )
+ else -> null
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ private fun SignalIconModel.Cellular.levelDescriptionRes() =
+ when (level) {
+ 0 -> R.string.accessibility_no_signal
+ 1 -> R.string.accessibility_one_bar
+ 2 -> R.string.accessibility_two_bars
+ 3 -> R.string.accessibility_three_bars
+ 4 -> {
+ if (numberOfLevels == 6) {
+ R.string.accessibility_four_bars
+ } else {
+ R.string.accessibility_signal_full
+ }
+ }
+ 5 -> {
+ if (numberOfLevels == 6) {
+ R.string.accessibility_signal_full
+ } else {
+ R.string.accessibility_no_signal
+ }
+ }
+ else -> R.string.accessibility_no_signal
+ }
+
+ private val showNetworkTypeIcon: Flow<Boolean> =
+ combine(
+ iconInteractor.isDataConnected,
+ iconInteractor.isDataEnabled,
+ iconInteractor.alwaysShowDataRatIcon,
+ iconInteractor.mobileIsDefault,
+ iconInteractor.carrierNetworkChangeActive,
+ ) { dataConnected, dataEnabled, alwaysShow, mobileIsDefault, carrierNetworkChange ->
+ alwaysShow ||
+ (!carrierNetworkChange && (dataEnabled && dataConnected && mobileIsDefault))
+ }
+ .distinctUntilChanged()
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnName = "showNetworkTypeIcon",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val networkTypeIcon: Flow<Icon.Resource?> =
+ combine(iconInteractor.networkTypeIconGroup, showNetworkTypeIcon) {
+ networkTypeIconGroup,
+ shouldShow ->
+ val desc =
+ if (networkTypeIconGroup.contentDescription != 0)
+ ContentDescription.Resource(networkTypeIconGroup.contentDescription)
+ else null
+ val icon =
+ if (networkTypeIconGroup.iconId != 0)
+ Icon.Resource(networkTypeIconGroup.iconId, desc)
+ else null
+ return@combine when {
+ !shouldShow -> null
+ else -> icon
+ }
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val networkTypeBackground =
+ iconInteractor.showSliceAttribution
+ .map {
+ when {
+ it && NewStatusBarIcons.isEnabled ->
+ Icon.Resource(R.drawable.mobile_network_type_background_updated, null)
+ it -> Icon.Resource(R.drawable.mobile_network_type_background, null)
+ else -> null
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val roaming: StateFlow<Boolean> =
+ iconInteractor.isRoaming
+ .logDiffsForTable(
+ iconInteractor.tableLogBuffer,
+ columnName = "roaming",
+ initialValue = false,
+ )
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ private val activity: Flow<DataActivityModel?> =
+ if (!constants.shouldShowActivityConfig) {
+ flowOf(null)
+ } else {
+ iconInteractor.activity
+ }
+
+ override val activityInVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityIn ?: false }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityOutVisible: Flow<Boolean> =
+ activity
+ .map { it?.hasActivityOut ?: false }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val activityContainerVisible: Flow<Boolean> =
+ if (statusBarStaticInoutIndicators()) {
+ flowOf(constants.shouldShowActivityConfig)
+ } else {
+ activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
new file mode 100644
index 000000000000..a65540738828
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.systemui.coroutines.newTracingContext
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * View model for describing the system's current mobile cellular connections. The result is a list
+ * of [MobileIconViewModel]s which describe the individual icons and can be bound to
+ * [ModernStatusBarMobileView].
+ */
+@SysUISingleton
+class MobileIconsViewModelKairos
+@Inject
+constructor(
+ val logger: MobileViewLogger,
+ private val verboseLogger: VerboseMobileViewLogger,
+ private val interactor: MobileIconsInteractor,
+ private val airplaneModeInteractor: AirplaneModeInteractor,
+ private val constants: ConnectivityConstants,
+ @Background private val scope: CoroutineScope,
+) {
+ @VisibleForTesting
+ val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>()
+
+ val activeMobileDataSubscriptionId: StateFlow<Int?> = interactor.activeMobileDataSubscriptionId
+
+ val subscriptionIdsFlow: StateFlow<List<Int>> =
+ interactor.filteredSubscriptions
+ .mapLatest { subscriptions ->
+ subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
+
+ val mobileSubViewModels: StateFlow<List<MobileIconViewModelCommon>> =
+ subscriptionIdsFlow
+ .map { ids -> ids.map { commonViewModelForSub(it) } }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+ private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> =
+ mobileSubViewModels
+ .map {
+ if (it.isEmpty()) {
+ null
+ } else {
+ // Mobile icons get reversed by [StatusBarIconController], so the last element
+ // in this list will show up visually first.
+ it.last()
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ /**
+ * A flow that emits `true` if the mobile sub that's displayed first visually is showing its
+ * network type icon and `false` otherwise.
+ */
+ val firstMobileSubShowingNetworkTypeIcon: StateFlow<Boolean> =
+ firstMobileSubViewModel
+ .flatMapLatest { firstMobileSubViewModel ->
+ firstMobileSubViewModel?.networkTypeIcon?.map { it != null } ?: flowOf(false)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ val isStackable: StateFlow<Boolean> = interactor.isStackable
+
+ init {
+ scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } }
+ }
+
+ fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel {
+ val common = commonViewModelForSub(subId)
+ return LocationBasedMobileViewModel.viewModelForLocation(
+ common,
+ interactor.getMobileConnectionInteractorForSubId(subId),
+ verboseLogger,
+ location,
+ scope,
+ )
+ }
+
+ private fun commonViewModelForSub(subId: Int): MobileIconViewModelCommon {
+ return reuseCache.getOrPut(subId) { createViewModel(subId) }.first
+ }
+
+ private fun createViewModel(subId: Int): Pair<MobileIconViewModel, CoroutineScope> {
+ // Create a child scope so we can cancel it
+ val vmScope = scope.createChildScope(newTracingContext("MobileIconViewModel"))
+ val vm =
+ MobileIconViewModel(
+ subId,
+ interactor.getMobileConnectionInteractorForSubId(subId),
+ airplaneModeInteractor,
+ constants,
+ vmScope,
+ )
+
+ return Pair(vm, vmScope)
+ }
+
+ private fun CoroutineScope.createChildScope(extraContext: CoroutineContext) =
+ CoroutineScope(coroutineContext + Job(coroutineContext[Job]) + extraContext)
+
+ private fun invalidateCaches(subIds: List<Int>) {
+ reuseCache.keys
+ .filter { !subIds.contains(it) }
+ .forEach { id ->
+ reuseCache
+ .remove(id)
+ // Cancel the view model's scope after removing it
+ ?.second
+ ?.cancel()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt
new file mode 100644
index 000000000000..2dbb02c8f095
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StackedMobileIconViewModelKairos
+@AssistedInject
+constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() {
+ private val hydrator = Hydrator("StackedMobileIconViewModel")
+
+ private val isStackable: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "isStackable",
+ source = mobileIconsViewModel.isStackable,
+ initialValue = false,
+ )
+
+ private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> =
+ combine(
+ mobileIconsViewModel.mobileSubViewModels,
+ mobileIconsViewModel.activeMobileDataSubscriptionId,
+ ) { viewModels, activeSubId ->
+ // Sort to get the active subscription first, if it's set
+ viewModels.sortedByDescending { it.subscriptionId == activeSubId }
+ }
+
+ val dualSim: DualSim? by
+ hydrator.hydratedStateOf(
+ traceName = "dualSim",
+ source =
+ iconViewModelFlow.flatMapLatest { viewModels ->
+ combine(viewModels.map { it.icon }) { icons ->
+ icons
+ .toList()
+ .filterIsInstance<SignalIconModel.Cellular>()
+ .takeIf { it.size == 2 }
+ ?.let { DualSim(it[0], it[1]) }
+ }
+ },
+ initialValue = null,
+ )
+
+ val networkTypeIcon: Icon.Resource? by
+ hydrator.hydratedStateOf(
+ traceName = "networkTypeIcon",
+ source =
+ iconViewModelFlow.flatMapLatest { viewModels ->
+ viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null)
+ },
+ initialValue = null,
+ )
+
+ val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null }
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): StackedMobileIconViewModelKairos
+ }
+
+ data class DualSim(
+ val primary: SignalIconModel.Cellular,
+ val secondary: SignalIconModel.Cellular,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index d3af1e5b65fe..6d959be1c5f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.policy
import android.app.ActivityOptions
+import android.app.Flags.notificationsRedesignTemplates
import android.app.Notification
import android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY
import android.app.PendingIntent
@@ -53,7 +54,6 @@ import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
-import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions
import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
@@ -397,16 +397,21 @@ constructor(
delayOnClickListener: Boolean,
packageContext: Context,
): Button {
- val isMagicAction = Flags.notificationMagicActionsTreatment() &&
+ val isMagicAction =
+ Flags.notificationMagicActionsTreatment() &&
smartActions.fromAssistant &&
action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false)
- val layoutRes = if (isMagicAction) {
- R.layout.magic_action_button
- } else {
- R.layout.smart_action_button
- }
- return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
- as Button)
+ val layoutRes =
+ if (isMagicAction) {
+ R.layout.magic_action_button
+ } else {
+ if (notificationsRedesignTemplates()) {
+ R.layout.notification_2025_smart_action_button
+ } else {
+ R.layout.smart_action_button
+ }
+ }
+ return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
text = action.title
@@ -435,7 +440,6 @@ constructor(
// Mark this as an Action button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION
}
-
}
private fun onSmartActionClick(
@@ -499,9 +503,11 @@ constructor(
replyIndex: Int,
choice: CharSequence,
delayOnClickListener: Boolean,
- ): Button =
- (LayoutInflater.from(parent.context).inflate(R.layout.smart_reply_button, parent, false)
- as Button)
+ ): Button {
+ val layoutRes =
+ if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button
+ else R.layout.smart_reply_button
+ return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
text = choice
val onClickListener =
@@ -531,6 +537,7 @@ constructor(
// Mark this as a Reply button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY
}
+ }
private fun onSmartReplyClick(
entry: NotificationEntry,
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
new file mode 100644
index 000000000000..7dc6fa8e9f70
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects;
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.Gravity
+import android.view.WindowInsets
+import android.view.WindowManager
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
+import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@SysUISingleton
+class TopLevelWindowEffects @Inject constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val windowManager: ViewCaptureAwareWindowManager,
+ private val squeezeEffectInteractor: SqueezeEffectInteractor
+) : CoreStartable {
+
+ override fun start() {
+ applicationScope.launch {
+ var root: EffectsWindowRoot? = null
+ squeezeEffectInteractor.isSqueezeEffectEnabled.collectLatest { enabled ->
+ // TODO: move window ops to a separate UI thread
+ if (enabled && root == null) {
+ root = EffectsWindowRoot(context)
+ root?.let { windowManager.addView(it, getWindowManagerLayoutParams()) }
+ } else if (root?.isAttachedToWindow == true) {
+ windowManager.removeView(root)
+ root = null
+ }
+ }
+ }
+ }
+
+ private fun getWindowManagerLayoutParams(): WindowManager.LayoutParams {
+ val lp = WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+ PixelFormat.TRANSPARENT
+ )
+
+ lp.privateFlags = lp.privateFlags or
+ (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ or WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+ or WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED
+ or WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED
+ or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+
+ lp.layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+
+ lp.title = "TopLevelWindowEffects"
+ lp.fitInsetsTypes = WindowInsets.Type.systemOverlays()
+ lp.gravity = Gravity.TOP
+
+ return lp
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt
new file mode 100644
index 000000000000..5a2af9228771
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/SqueezeEffectRepositoryModule.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.dagger
+
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepository
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface SqueezeEffectRepositoryModule {
+
+ @Binds
+ fun squeezeEffectRepository(
+ squeezeEffectRepositoryImpl: SqueezeEffectRepositoryImpl
+ ) : SqueezeEffectRepository
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt
index 70c4636e25c5..6fbfedc94774 100644
--- a/packages/SystemUI/src/com/android/systemui/effects/dagger/TopLevelWindowEffectsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/dagger/TopLevelWindowEffectsModule.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.effects.dagger
+package com.android.systemui.topwindoweffects.dagger
import com.android.systemui.CoreStartable
-import com.android.systemui.effects.TopLevelWindowEffects
+import com.android.systemui.topwindoweffects.TopLevelWindowEffects
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt
new file mode 100644
index 000000000000..9e0b25633d5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import kotlinx.coroutines.flow.Flow
+
+interface SqueezeEffectRepository {
+ val isSqueezeEffectEnabled: Flow<Boolean>
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt
new file mode 100644
index 000000000000..9e0feed35016
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryImpl.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
+import com.android.internal.R
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.Flags
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+
+@SysUISingleton
+class SqueezeEffectRepositoryImpl @Inject constructor(
+ @Background private val bgHandler: Handler?,
+ @Background private val bgCoroutineContext: CoroutineContext,
+ private val globalSettings: GlobalSettings
+) : SqueezeEffectRepository {
+
+ override val isSqueezeEffectEnabled: Flow<Boolean> = conflatedCallbackFlow {
+ val observer = object : ContentObserver(bgHandler) {
+ override fun onChange(selfChange: Boolean) {
+ trySendWithFailureLogging(squeezeEffectEnabled, TAG,
+ "updated isSqueezeEffectEnabled")
+ }
+ }
+ trySendWithFailureLogging(squeezeEffectEnabled, TAG, "init isSqueezeEffectEnabled")
+ globalSettings.registerContentObserverAsync(POWER_BUTTON_LONG_PRESS, observer)
+ awaitClose { globalSettings.unregisterContentObserverAsync(observer) }
+ }.flowOn(bgCoroutineContext)
+
+ private val squeezeEffectEnabled
+ get() = Flags.enableLppSqueezeEffect() && globalSettings.getInt(
+ POWER_BUTTON_LONG_PRESS, R.integer.config_longPressOnPowerBehavior
+ ) == 5 // 5 corresponds to launch assistant in config_longPressOnPowerBehavior
+
+ companion object {
+ private const val TAG = "SqueezeEffectRepository"
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt
new file mode 100644
index 000000000000..879fde769aee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractor.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.topwindoweffects.data.repository.SqueezeEffectRepository
+import javax.inject.Inject
+
+@SysUISingleton
+class SqueezeEffectInteractor @Inject constructor(
+ squeezeEffectRepository: SqueezeEffectRepository
+) {
+ val isSqueezeEffectEnabled = squeezeEffectRepository.isSqueezeEffectEnabled
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
new file mode 100644
index 000000000000..0826917c5cb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.ui.compose
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.AbstractComposeView
+import com.android.systemui.compose.ComposeInitializer
+
+class EffectsWindowRoot : AbstractComposeView {
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int,
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun onAttachedToWindow() {
+ ComposeInitializer.onAttachedToWindow(this)
+ super.onAttachedToWindow()
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ ComposeInitializer.onDetachedFromWindow(this)
+ }
+
+ @Composable
+ override fun Content() {
+ SqueezeEffect()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
new file mode 100644
index 000000000000..124724000aaf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.ui.compose
+
+import androidx.compose.runtime.Composable
+
+@Composable
+fun SqueezeEffect() {
+ // TODO: add squeeze effect
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 205ccea657df..9ad2235965bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -406,11 +406,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
}
@Override
- int getHeaderIconSize() {
- return 10;
- }
-
- @Override
CharSequence getHeaderText() {
return mHeaderTitle;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
index f0902e35b837..f1bf7c0bcf13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
@@ -50,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.media.BluetoothMediaDevice;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -152,9 +153,9 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase {
volumePanelGlobalStateInteractor,
mUserTracker);
mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
- mMediaOutputBroadcastDialog =
- new MediaOutputBroadcastDialog(
- mContext, false, mBroadcastSender, mMediaSwitchingController);
+ mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false,
+ mBroadcastSender, mMediaSwitchingController, mContext.getMainExecutor(),
+ ThreadUtils.getBackgroundExecutor());
mMediaOutputBroadcastDialog.show();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index d3ecb3d8c944..420fd6e33abc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -53,6 +53,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -455,6 +456,8 @@ public class MediaOutputDialogTest extends SysuiTestCase {
controller,
mDialogTransitionAnimator,
mUiEventLogger,
+ mContext.getMainExecutor(),
+ ThreadUtils.getBackgroundExecutor(),
true);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
index 2d3f538689b3..155059ea5ed9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/LauncherProxyServiceTest.kt
@@ -36,6 +36,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
+import com.android.systemui.log.assertLogsWtf
import com.android.systemui.model.sysUiState
import com.android.systemui.navigationbar.NavigationBarController
import com.android.systemui.navigationbar.NavigationModeController
@@ -221,7 +222,7 @@ class LauncherProxyServiceTest : SysuiTestCase() {
`when`(processWrapper.isSystemUser).thenReturn(false)
`when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false)
val spyContext = spy(context)
- val ops = createLauncherProxyService(spyContext)
+ val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result
ops.startConnectionToCurrentUser()
verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any())
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 81213caaa5f4..3570cf827808 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -77,7 +77,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.util.time.FakeSystemClock;
@@ -1795,7 +1794,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
// THEN an exception is NOT thrown directly, but a WTF IS logged.
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1818,7 +1817,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1844,13 +1843,13 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
// Note: dispatchBuild itself triggers a non-reentrant pipeline run.
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1874,7 +1873,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_1);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1897,7 +1896,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_1);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1922,7 +1921,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1945,7 +1944,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
@@ -1970,7 +1969,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
});
@@ -1993,7 +1992,7 @@ public class ShadeListBuilderTest extends SysuiTestCase {
addNotif(0, PACKAGE_2);
invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
- LogAssertKt.assertLogsWtfs(() -> {
+ LogAssertKt.assertRunnableLogsWtfs(() -> {
Assert.assertThrows(IllegalStateException.class, () -> {
dispatchBuild();
runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index cd2ea7d25699..4315c0f638ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -936,11 +937,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setSensitive(/* sensitive= */true, /* hideSensitive= */false);
row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true);
@@ -949,11 +952,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(false);
// THEN
@@ -961,11 +966,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
// THEN
@@ -973,12 +980,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setIgnoreLockscreenConstraints(true);
@@ -987,12 +996,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setSaveSpaceOnLockscreen(true);
@@ -1001,12 +1012,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setSaveSpaceOnLockscreen(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 89b7bee7f286..a3616d20e11f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -128,6 +128,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.media.NotificationMediaManager;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.notetask.NoteTaskController;
import com.android.systemui.plugins.ActivityStarter;
@@ -164,7 +165,6 @@ import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.LightRevealScrim;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeDepthController;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 0ba7c8574d85..a3d01355ac4c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -47,6 +47,7 @@ import com.android.systemui.log.dagger.BiometricLog
import com.android.systemui.log.dagger.BroadcastDispatcherLog
import com.android.systemui.log.dagger.FaceAuthLog
import com.android.systemui.log.dagger.SceneFrameworkLog
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.model.SysUiState
import com.android.systemui.plugins.ActivityStarter
@@ -57,7 +58,6 @@ import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationShadeDepthController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
index 807bc82cbf4e..c9e25c31faa0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
@@ -34,7 +34,7 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository {
_isPowerButtonDown.value = isDown
}
- fun setPowerButtonBeingLongPressed(isLongPressed: Boolean) {
+ fun setPowerButtonLongPressed(isLongPressed: Boolean) {
_isPowerButtonLongPressed.value = isLongPressed
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt
index 2797b4409ff0..bf456978d983 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModelKosmos.kt
@@ -48,5 +48,6 @@ val Kosmos.deviceEntryBackgroundViewModel by Fixture {
primaryBouncerToLockscreenTransitionViewModel,
lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
glanceableHubToAodTransitionViewModel = glanceableHubToAodTransitionViewModel,
+ glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 27ca0f867855..a9aa8cd5a7f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -84,6 +84,7 @@ val Kosmos.keyguardRootViewModel by Fixture {
occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
+ occludedToPrimaryBouncerTransitionViewModel = occludedToPrimaryBouncerTransitionViewModel,
offToLockscreenTransitionViewModel = offToLockscreenTransitionViewModel,
primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel,
primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
index 5e67182d7353..b41ceff5f581 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/log/LogAssert.kt
@@ -13,89 +13,103 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.systemui.log
import android.util.Log
import android.util.Log.TerribleFailureHandler
-import junit.framework.Assert
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.Callable
-/** Asserts that the given block does not make a call to Log.wtf */
-fun assertDoesNotLogWtf(
+/** Asserts that [notLoggingBlock] does not make a call to [Log.wtf] */
+fun <T> assertDoesNotLogWtf(
message: String = "Expected Log.wtf not to be called",
- notLoggingBlock: () -> Unit,
-) {
+ notLoggingBlock: () -> T,
+): T {
var caught: TerribleFailureLog? = null
val newHandler = TerribleFailureHandler { tag, failure, system ->
caught = TerribleFailureLog(tag, failure, system)
}
val oldHandler = Log.setWtfHandler(newHandler)
- try {
- notLoggingBlock()
- } finally {
- Log.setWtfHandler(oldHandler)
- }
+ val result =
+ try {
+ notLoggingBlock()
+ } finally {
+ Log.setWtfHandler(oldHandler)
+ }
caught?.let { throw AssertionError("$message: $it", it.failure) }
+ return result
}
-fun assertDoesNotLogWtf(
- message: String = "Expected Log.wtf not to be called",
- notLoggingRunnable: Runnable,
-) = assertDoesNotLogWtf(message = message) { notLoggingRunnable.run() }
-
-/**
- * Assert that the given block makes a call to Log.wtf
- *
- * @return the details of the log
- */
-fun assertLogsWtf(
+/** Assert that [loggingBlock] makes a call to [Log.wtf] */
+@JvmOverloads
+fun <T> assertLogsWtf(
message: String = "Expected Log.wtf to be called",
allowMultiple: Boolean = false,
- loggingBlock: () -> Unit,
-): TerribleFailureLog {
- var caught: TerribleFailureLog? = null
- var count = 0
+ loggingBlock: () -> T,
+): WtfBlockResult<T> {
+ val caught = mutableListOf<TerribleFailureLog>()
val newHandler = TerribleFailureHandler { tag, failure, system ->
- if (caught == null) {
- caught = TerribleFailureLog(tag, failure, system)
- }
- count++
+ caught.add(TerribleFailureLog(tag, failure, system))
}
val oldHandler = Log.setWtfHandler(newHandler)
- try {
- loggingBlock()
- } finally {
- Log.setWtfHandler(oldHandler)
- }
- Assert.assertNotNull(message, caught)
- if (!allowMultiple && count != 1) {
- Assert.fail("Unexpectedly caught Log.Wtf $count times; expected only 1. First: $caught")
+ val result =
+ try {
+ loggingBlock()
+ } finally {
+ Log.setWtfHandler(oldHandler)
+ }
+ assertWithMessage(message).that(caught).isNotEmpty()
+ if (!allowMultiple) {
+ assertWithMessage("Unexpectedly caught Log.Wtf multiple times").that(caught).hasSize(1)
}
- return caught!!
+ return WtfBlockResult(caught, result)
}
+/** Assert that [loggingCallable] makes a call to [Log.wtf] */
@JvmOverloads
-fun assertLogsWtf(
+fun <T> assertLogsWtf(
message: String = "Expected Log.wtf to be called",
allowMultiple: Boolean = false,
- loggingRunnable: Runnable,
-): TerribleFailureLog =
- assertLogsWtf(message = message, allowMultiple = allowMultiple) { loggingRunnable.run() }
+ loggingCallable: Callable<T>,
+): WtfBlockResult<T> =
+ assertLogsWtf(message = message, allowMultiple = allowMultiple, loggingCallable::call)
-fun assertLogsWtfs(
+/** Assert that [loggingBlock] makes at least one call to [Log.wtf] */
+@JvmOverloads
+fun <T> assertLogsWtfs(
message: String = "Expected Log.wtf to be called once or more",
- loggingBlock: () -> Unit,
-): TerribleFailureLog = assertLogsWtf(message, allowMultiple = true, loggingBlock)
+ loggingBlock: () -> T,
+): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingBlock)
+/** Assert that [loggingCallable] makes at least one call to [Log.wtf] */
@JvmOverloads
-fun assertLogsWtfs(
+fun <T> assertLogsWtfs(
message: String = "Expected Log.wtf to be called once or more",
- loggingRunnable: Runnable,
-): TerribleFailureLog = assertLogsWtfs(message) { loggingRunnable.run() }
+ loggingCallable: Callable<T>,
+): WtfBlockResult<T> = assertLogsWtf(message, allowMultiple = true, loggingCallable)
/** The data passed to [TerribleFailureHandler.onTerribleFailure] */
data class TerribleFailureLog(
val tag: String,
val failure: Log.TerribleFailure,
- val system: Boolean
+ val system: Boolean,
)
+
+/** The [Log.wtf] logs and return value of the block */
+data class WtfBlockResult<T>(val logs: List<TerribleFailureLog>, val result: T)
+
+/** Assert that [loggingRunnable] makes a call to [Log.wtf] */
+@JvmOverloads
+fun assertRunnableLogsWtf(
+ message: String = "Expected Log.wtf to be called",
+ allowMultiple: Boolean = false,
+ loggingRunnable: Runnable,
+): WtfBlockResult<Unit> =
+ assertLogsWtf(message = message, allowMultiple = allowMultiple) { loggingRunnable.run() }
+
+/** Assert that [loggingRunnable] makes at least one call to [Log.wtf] */
+@JvmOverloads
+fun assertRunnableLogsWtfs(
+ message: String = "Expected Log.wtf to be called once or more",
+ loggingRunnable: Runnable,
+): WtfBlockResult<Unit> = assertRunnableLogsWtf(message, allowMultiple = true, loggingRunnable)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index ff4fbf9f0e94..7f012ba25ab9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -37,6 +37,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.media.NotificationMediaManager
import com.android.systemui.media.controls.util.MediaFeatureFlag
import com.android.systemui.media.dialog.MediaOutputDialogManager
import com.android.systemui.plugins.ActivityStarter
@@ -45,7 +46,6 @@ import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.system.ActivityManagerWrapper
import com.android.systemui.shared.system.DevicePolicyManagerWrapper
import com.android.systemui.shared.system.PackageManagerWrapper
-import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.RankingBuilder
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt
new file mode 100644
index 000000000000..d9a327b99c7f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileDomainInteractorKairosKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
+import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
+import com.android.systemui.util.carrierConfigTracker
+
+@ExperimentalKairosApi
+val Kosmos.mobileIconsInteractorKairos by ActivatedKairosFixture {
+ MobileIconsInteractorKairosImpl(
+ mobileConnectionsRepositoryKairos,
+ carrierConfigTracker,
+ logcatTableLogBuffer(this),
+ connectivityRepository,
+ userSetupRepository,
+ applicationContext,
+ featureFlagsClassic,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt
new file mode 100644
index 000000000000..3ee33802e9d5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.stackedMobileIconViewModelKairos by
+ Kosmos.Fixture { StackedMobileIconViewModelKairos(mobileIconsViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt
new file mode 100644
index 000000000000..2d2a81586515
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeSqueezeEffectRepository : SqueezeEffectRepository {
+ override val isSqueezeEffectEnabled = MutableStateFlow(false)
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt
new file mode 100644
index 000000000000..aa8bb6b1e104
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeSqueezeEffectRepository by Kosmos.Fixture { FakeSqueezeEffectRepository() } \ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt
new file mode 100644
index 000000000000..58473693954c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/CarrierConfigTrackerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mockFixture
+
+var Kosmos.carrierConfigTracker: CarrierConfigTracker by mockFixture()
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 3e2c4051b792..f25ae6a34242 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -128,28 +128,6 @@ public class RavenwoodUtils {
runOnHandlerSync(getMainHandler(), r);
}
- public static class MockitoHelper {
- private MockitoHelper() {
- }
-
- /**
- * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
- * method on. (Maybe we should do it in Ravenizer.)
- */
- public static void verifyZeroInteractions(Object... mocks) {
- if (RavenwoodRule.isOnRavenwood()) {
- // Mockito 4 or later
- reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
- .callStatic(new Object[]{mocks});
- } else {
- // Mockito 2
- reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
- .callStatic(new Object[]{mocks});
- }
- }
- }
-
-
/**
* Wrap the given {@link Supplier} to become memoized.
*
diff --git a/ravenwood/scripts/extract-last-soong-commands.py b/ravenwood/scripts/extract-last-soong-commands.py
index 0629b77029e0..b8d6f2042389 100755
--- a/ravenwood/scripts/extract-last-soong-commands.py
+++ b/ravenwood/scripts/extract-last-soong-commands.py
@@ -55,7 +55,7 @@ def main(args):
if s.startswith("verbose"):
continue
- if re.match('^\[.*bootstrap blueprint', s):
+ if re.match('^\\[.*bootstrap blueprint', s):
continue
s = s.rstrip()
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 739ea0df87ab..cc93d0887d89 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -248,7 +248,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
private boolean isPaused() {
return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused()
- && !mAutoclickTypePanel.isHovered();
+ && !isHovered();
+ }
+
+ private boolean isHovered() {
+ return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isHovered();
}
private void cancelPendingClick() {
@@ -495,6 +499,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
private int mEventPolicyFlags;
/** Current meta state. This value will be used as meta state for click event sequence. */
private int mMetaState;
+ /** Last observed panel hovered state when click was scheduled. */
+ private boolean mHoveredState;
/**
* The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null.
@@ -648,6 +654,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
}
mLastMotionEvent = MotionEvent.obtain(event);
mEventPolicyFlags = policyFlags;
+ mHoveredState = isHovered();
if (useAsAnchor) {
final int pointerIndex = mLastMotionEvent.getActionIndex();
@@ -729,14 +736,18 @@ public class AutoclickController extends BaseEventStreamTransformation {
final long now = SystemClock.uptimeMillis();
- // TODO(b/395094903): always triggers left-click when the cursor hovers over the
- // autoclick type panel, to always allow users to change a different click type.
- // Otherwise, if one chooses the right-click, this user won't be able to rely on
- // autoclick to select other click types.
- final int actionButton =
- mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
- ? BUTTON_SECONDARY
- : BUTTON_PRIMARY;
+ int actionButton;
+ if (mHoveredState) {
+ // Always triggers left-click when the cursor hovers over the autoclick type
+ // panel, to always allow users to change a different click type. Otherwise, if
+ // one chooses the right-click, this user won't be able to rely on autoclick to
+ // select other click types.
+ actionButton = BUTTON_PRIMARY;
+ } else {
+ actionButton = mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
+ ? BUTTON_SECONDARY
+ : BUTTON_PRIMARY;
+ }
MotionEvent downEvent =
MotionEvent.obtain(
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6fdb2b6b83f7..6515b237519a 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -64,6 +64,7 @@ import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREF
import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_PCC;
import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_UNKNOWN;
import static com.android.server.autofill.FillResponseEventLogger.HAVE_SAVE_TRIGGER_ID;
+import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_CANCELLED;
import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_FAILURE;
import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SESSION_DESTROYED;
import static com.android.server.autofill.FillResponseEventLogger.RESPONSE_STATUS_SUCCESS;
@@ -1416,6 +1417,15 @@ final class Session
// Remove the FillContext as there will never be a response for the service
if (canceledRequest != INVALID_REQUEST_ID && mContexts != null) {
+ // Start a new FillResponse logger for the cancellation case.
+ mFillResponseEventLogger.startLogForNewResponse();
+ mFillResponseEventLogger.maybeSetRequestId(canceledRequest);
+ mFillResponseEventLogger.maybeSetAppPackageUid(uid);
+ mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_CANCELLED);
+ mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis(
+ (int) (SystemClock.elapsedRealtime() - mLatencyBaseTime));
+ mFillResponseEventLogger.logAndEndEvent();
+
final int numContexts = mContexts.size();
// It is most likely the last context, hence search backwards
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 1263146fe405..20f103cdfab4 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -589,6 +589,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
monitoringExtras);
Slog.e(TAG, "Failure getting next package name");
EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+ mStatus = BackupTransport.TRANSPORT_ERROR;
nextState = UnifiedRestoreState.FINAL;
return;
} else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index a73a991bc6a6..658ea4c27e4c 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -130,8 +130,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class AdbDebuggingManager {
private static final String TAG = AdbDebuggingManager.class.getSimpleName();
- private static final boolean DEBUG = false;
- private static final boolean MDNS_DEBUG = false;
private static final String ADBD_SOCKET = "adbd";
private static final String ADB_DIRECTORY = "misc/adb";
@@ -156,8 +154,6 @@ public class AdbDebuggingManager {
@Nullable private final File mUserKeyFile;
@Nullable private final File mTempKeysFile;
- private static final String WIFI_PERSISTENT_CONFIG_PROPERTY =
- "persist.adb.tls_server.enable";
private static final String WIFI_PERSISTENT_GUID =
"persist.adb.wifi.guid";
private static final int PAIRING_CODE_LENGTH = 6;
@@ -261,12 +257,10 @@ public class AdbDebuggingManager {
mHandler.sendMessage(msg);
boolean paired = native_pairing_wait();
- if (DEBUG) {
- if (mPublicKey != null) {
- Slog.i(TAG, "Pairing succeeded key=" + mPublicKey);
- } else {
- Slog.i(TAG, "Pairing failed");
- }
+ if (mPublicKey != null) {
+ Slog.i(TAG, "Pairing succeeded key=" + mPublicKey);
+ } else {
+ Slog.i(TAG, "Pairing failed");
}
mNsdManager.unregisterService(this);
@@ -307,7 +301,7 @@ public class AdbDebuggingManager {
@Override
public void onServiceRegistered(NsdServiceInfo serviceInfo) {
- if (MDNS_DEBUG) Slog.i(TAG, "Registered pairing service: " + serviceInfo);
+ Slog.i(TAG, "Registered pairing service: " + serviceInfo);
}
@Override
@@ -319,7 +313,7 @@ public class AdbDebuggingManager {
@Override
public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
- if (MDNS_DEBUG) Slog.i(TAG, "Unregistered pairing service: " + serviceInfo);
+ Slog.i(TAG, "Unregistered pairing service: " + serviceInfo);
}
@Override
@@ -354,7 +348,7 @@ public class AdbDebuggingManager {
@Override
public void run() {
- if (DEBUG) Slog.d(TAG, "Starting adb port property poller");
+ Slog.d(TAG, "Starting adb port property poller");
// Once adbwifi is enabled, we poll the service.adb.tls.port
// system property until we get the port, or -1 on failure.
// Let's also limit the polling to 10 seconds, just in case
@@ -390,7 +384,7 @@ public class AdbDebuggingManager {
class PortListenerImpl implements AdbConnectionPortListener {
public void onPortReceived(int port) {
- if (DEBUG) Slog.d(TAG, "Received tls port=" + port);
+ Slog.d(TAG, "Received tls port=" + port);
Message msg = mHandler.obtainMessage(port > 0
? AdbDebuggingHandler.MSG_SERVER_CONNECTED
: AdbDebuggingHandler.MSG_SERVER_DISCONNECTED);
@@ -419,11 +413,11 @@ public class AdbDebuggingManager {
@Override
public void run() {
- if (DEBUG) Slog.d(TAG, "Entering thread");
+ Slog.d(TAG, "Entering thread");
while (true) {
synchronized (this) {
if (mStopped) {
- if (DEBUG) Slog.d(TAG, "Exiting thread");
+ Slog.d(TAG, "Exiting thread");
return;
}
try {
@@ -448,7 +442,7 @@ public class AdbDebuggingManager {
LocalSocketAddress.Namespace.RESERVED);
mInputStream = null;
- if (DEBUG) Slog.d(TAG, "Creating socket");
+ Slog.d(TAG, "Creating socket");
mSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
mSocket.connect(address);
@@ -549,7 +543,7 @@ public class AdbDebuggingManager {
}
private void closeSocketLocked() {
- if (DEBUG) Slog.d(TAG, "Closing socket");
+ Slog.d(TAG, "Closing socket");
try {
if (mOutputStream != null) {
mOutputStream.close();
@@ -859,7 +853,7 @@ public class AdbDebuggingManager {
private void startAdbDebuggingThread() {
++mAdbEnabledRefCount;
- if (DEBUG) Slog.i(TAG, "startAdbDebuggingThread ref=" + mAdbEnabledRefCount);
+ Slog.i(TAG, "startAdbDebuggingThread ref=" + mAdbEnabledRefCount);
if (mAdbEnabledRefCount > 1) {
return;
}
@@ -875,7 +869,7 @@ public class AdbDebuggingManager {
private void stopAdbDebuggingThread() {
--mAdbEnabledRefCount;
- if (DEBUG) Slog.i(TAG, "stopAdbDebuggingThread ref=" + mAdbEnabledRefCount);
+ Slog.i(TAG, "stopAdbDebuggingThread ref=" + mAdbEnabledRefCount);
if (mAdbEnabledRefCount > 0) {
return;
}
@@ -1093,7 +1087,7 @@ public class AdbDebuggingManager {
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, intentFilter);
- SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
mConnectionPortPoller =
new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener);
mConnectionPortPoller.start();
@@ -1101,7 +1095,7 @@ public class AdbDebuggingManager {
startAdbDebuggingThread();
mAdbWifiEnabled = true;
- if (DEBUG) Slog.i(TAG, "adb start wireless adb");
+ Slog.i(TAG, "adb start wireless adb");
break;
}
case MSG_ADBDWIFI_DISABLE:
@@ -1143,7 +1137,7 @@ public class AdbDebuggingManager {
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, intentFilter);
- SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
mConnectionPortPoller =
new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener);
mConnectionPortPoller.start();
@@ -1151,7 +1145,7 @@ public class AdbDebuggingManager {
startAdbDebuggingThread();
mAdbWifiEnabled = true;
- if (DEBUG) Slog.i(TAG, "adb start wireless adb");
+ Slog.i(TAG, "adb start wireless adb");
break;
case MSG_ADBWIFI_DENY:
Settings.Global.putInt(mContentResolver,
@@ -1259,7 +1253,7 @@ public class AdbDebuggingManager {
break;
}
case MSG_ADBD_SOCKET_CONNECTED: {
- if (DEBUG) Slog.d(TAG, "adbd socket connected");
+ Slog.d(TAG, "adbd socket connected");
if (mAdbWifiEnabled) {
// In scenarios where adbd is restarted, the tls port may change.
mConnectionPortPoller =
@@ -1269,7 +1263,7 @@ public class AdbDebuggingManager {
break;
}
case MSG_ADBD_SOCKET_DISCONNECTED: {
- if (DEBUG) Slog.d(TAG, "adbd socket disconnected");
+ Slog.d(TAG, "adbd socket disconnected");
if (mConnectionPortPoller != null) {
mConnectionPortPoller.cancelAndWait();
mConnectionPortPoller = null;
@@ -1477,7 +1471,7 @@ public class AdbDebuggingManager {
}
private void updateUIPairCode(String code) {
- if (DEBUG) Slog.i(TAG, "updateUIPairCode: " + code);
+ Slog.i(TAG, "updateUIPairCode: " + code);
Intent intent = new Intent(AdbManager.WIRELESS_DEBUG_PAIRING_RESULT_ACTION);
intent.putExtra(AdbManager.WIRELESS_PAIRING_CODE_EXTRA, code);
diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java
index 55d8dba69626..aae48daa5dde 100644
--- a/services/core/java/com/android/server/adb/AdbService.java
+++ b/services/core/java/com/android/server/adb/AdbService.java
@@ -222,15 +222,14 @@ public class AdbService extends IAdbManager.Stub {
}
}
- private static final String TAG = "AdbService";
- private static final boolean DEBUG = false;
+ private static final String TAG = AdbService.class.getSimpleName();
/**
* The persistent property which stores whether adb is enabled or not.
* May also contain vendor-specific default functions for testing purposes.
*/
private static final String USB_PERSISTENT_CONFIG_PROPERTY = "persist.sys.usb.config";
- private static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable";
+ static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable";
private final Context mContext;
private final ContentResolver mContentResolver;
@@ -256,7 +255,7 @@ public class AdbService extends IAdbManager.Stub {
* SystemServer}.
*/
public void systemReady() {
- if (DEBUG) Slog.d(TAG, "systemReady");
+ Slog.d(TAG, "systemReady");
/*
* Use the normal bootmode persistent prop to maintain state of adb across
@@ -287,7 +286,7 @@ public class AdbService extends IAdbManager.Stub {
* Called in response to {@code SystemService.PHASE_BOOT_COMPLETED} from {@code SystemServer}.
*/
public void bootCompleted() {
- if (DEBUG) Slog.d(TAG, "boot completed");
+ Slog.d(TAG, "boot completed");
if (mDebuggingManager != null) {
mDebuggingManager.setAdbEnabled(mIsAdbUsbEnabled, AdbTransportType.USB);
mDebuggingManager.setAdbEnabled(mIsAdbWifiEnabled, AdbTransportType.WIFI);
@@ -429,17 +428,13 @@ public class AdbService extends IAdbManager.Stub {
@Override
public void registerCallback(IAdbCallback callback) throws RemoteException {
- if (DEBUG) {
- Slog.d(TAG, "Registering callback " + callback);
- }
+ Slog.d(TAG, "Registering callback " + callback);
mCallbacks.register(callback);
}
@Override
public void unregisterCallback(IAdbCallback callback) throws RemoteException {
- if (DEBUG) {
- Slog.d(TAG, "Unregistering callback " + callback);
- }
+ Slog.d(TAG, "Unregistering callback " + callback);
mCallbacks.unregister(callback);
}
/**
@@ -500,11 +495,8 @@ public class AdbService extends IAdbManager.Stub {
}
private void setAdbEnabled(boolean enable, byte transportType) {
- if (DEBUG) {
- Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled
- + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType="
- + transportType);
- }
+ Slog.d(TAG, "setAdbEnabled(" + enable + "), mIsAdbUsbEnabled=" + mIsAdbUsbEnabled
+ + ", mIsAdbWifiEnabled=" + mIsAdbWifiEnabled + ", transportType=" + transportType);
if (transportType == AdbTransportType.USB && enable != mIsAdbUsbEnabled) {
mIsAdbUsbEnabled = enable;
@@ -549,20 +541,14 @@ public class AdbService extends IAdbManager.Stub {
mDebuggingManager.setAdbEnabled(enable, transportType);
}
- if (DEBUG) {
- Slog.d(TAG, "Broadcasting enable = " + enable + ", type = " + transportType);
- }
+ Slog.d(TAG, "Broadcasting enable = " + enable + ", type = " + transportType);
mCallbacks.broadcast((callback) -> {
- if (DEBUG) {
- Slog.d(TAG, "Sending enable = " + enable + ", type = " + transportType
- + " to " + callback);
- }
+ Slog.d(TAG, "Sending enable = " + enable + ", type = " + transportType + " to "
+ + callback);
try {
callback.onDebuggingChanged(enable, transportType);
} catch (RemoteException ex) {
- if (DEBUG) {
- Slog.d(TAG, "Unable to send onDebuggingChanged:", ex);
- }
+ Slog.w(TAG, "Unable to send onDebuggingChanged:", ex);
}
});
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b0b34d0ab9c4..76ba0054583b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16190,14 +16190,16 @@ public class ActivityManagerService extends IActivityManager.Stub
return mUserController.switchUser(targetUserId);
}
+ @Nullable
@Override
- public String getSwitchingFromUserMessage() {
- return mUserController.getSwitchingFromSystemUserMessage();
+ public String getSwitchingFromUserMessage(@UserIdInt int userId) {
+ return mUserController.getSwitchingFromUserMessage(userId);
}
+ @Nullable
@Override
- public String getSwitchingToUserMessage() {
- return mUserController.getSwitchingToSystemUserMessage();
+ public String getSwitchingToUserMessage(@UserIdInt int userId) {
+ return mUserController.getSwitchingToUserMessage(userId);
}
@Override
@@ -16938,13 +16940,13 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
- mUserController.setSwitchingFromSystemUserMessage(switchingFromSystemUserMessage);
+ public void setSwitchingFromUserMessage(@UserIdInt int userId, @Nullable String message) {
+ mUserController.setSwitchingFromUserMessage(userId, message);
}
@Override
- public void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
- mUserController.setSwitchingToSystemUserMessage(switchingToSystemUserMessage);
+ public void setSwitchingToUserMessage(@UserIdInt int userId, @Nullable String message) {
+ mUserController.setSwitchingToUserMessage(userId, message);
}
@Override
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index db0562f5750a..508c01802156 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -810,7 +810,7 @@ class BroadcastProcessQueue {
* Return the broadcast being actively dispatched in this process.
*/
public @NonNull BroadcastRecord getActive() {
- return Objects.requireNonNull(mActive);
+ return Objects.requireNonNull(mActive, toString());
}
/**
@@ -818,7 +818,7 @@ class BroadcastProcessQueue {
* being actively dispatched in this process.
*/
public int getActiveIndex() {
- Objects.requireNonNull(mActive);
+ Objects.requireNonNull(mActive, toString());
return mActiveIndex;
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index c76a0d0ac59a..d276b9a94791 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -606,8 +606,9 @@ class BroadcastQueueImpl extends BroadcastQueue {
} else {
mRunningColdStart.reEnqueueActiveBroadcast();
}
- demoteFromRunningLocked(mRunningColdStart);
+ final BroadcastProcessQueue queue = mRunningColdStart;
clearRunningColdStart();
+ demoteFromRunningLocked(queue);
enqueueUpdateRunningList();
}
@@ -1527,6 +1528,15 @@ class BroadcastQueueImpl extends BroadcastQueue {
final int cookie = traceBegin("demoteFromRunning");
// We've drained running broadcasts; maybe move back to runnable
+ if (mRunningColdStart == queue) {
+ // TODO: b/399020479 - Remove wtf log once we identify the case where mRunningColdStart
+ // is not getting cleared.
+ // If this queue is mRunningColdStart, then it should have been cleared before
+ // it is demoted. Log a wtf if this isn't the case.
+ Slog.wtf(TAG, "mRunningColdStart has not been cleared; mRunningColdStart.app: "
+ + mRunningColdStart.app + " , queue.app: " + queue.app,
+ new IllegalStateException());
+ }
queue.makeActiveIdle();
queue.traceProcessEnd();
@@ -2332,12 +2342,6 @@ class BroadcastQueueImpl extends BroadcastQueue {
@VisibleForTesting
@GuardedBy("mService")
- @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull ProcessRecord app) {
- return removeProcessQueue(app.processName, app.info.uid);
- }
-
- @VisibleForTesting
- @GuardedBy("mService")
@Nullable BroadcastProcessQueue removeProcessQueue(@NonNull String processName,
int uid) {
BroadcastProcessQueue prev = null;
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 18f3500b2d56..40a9bbec3598 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -340,16 +340,16 @@ class UserController implements Handler.Callback {
private volatile ArraySet<String> mCurWaitingUserSwitchCallbacks;
/**
- * Messages for switching from {@link android.os.UserHandle#SYSTEM}.
+ * Message shown when switching from a user.
*/
@GuardedBy("mLock")
- private String mSwitchingFromSystemUserMessage;
+ private final SparseArray<String> mSwitchingFromUserMessage = new SparseArray<>();
/**
- * Messages for switching to {@link android.os.UserHandle#SYSTEM}.
+ * Message shown when switching to a user.
*/
@GuardedBy("mLock")
- private String mSwitchingToSystemUserMessage;
+ private final SparseArray<String> mSwitchingToUserMessage = new SparseArray<>();
/**
* Callbacks that are still active after {@link #getUserSwitchTimeoutMs}
@@ -2271,8 +2271,8 @@ class UserController implements Handler.Callback {
private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
// The dialog will show and then initiate the user switch by calling startUserInForeground
mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second,
- getSwitchingFromSystemUserMessageUnchecked(),
- getSwitchingToSystemUserMessageUnchecked(),
+ getSwitchingFromUserMessageUnchecked(fromToUserPair.first.id),
+ getSwitchingToUserMessageUnchecked(fromToUserPair.second.id),
/* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id));
}
@@ -3388,41 +3388,45 @@ class UserController implements Handler.Callback {
return mLockPatternUtils.isLockScreenDisabled(userId);
}
- void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
+ void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message) {
synchronized (mLock) {
- mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
+ mSwitchingFromUserMessage.put(user, message);
}
}
- void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
+ void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message) {
synchronized (mLock) {
- mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ mSwitchingToUserMessage.put(user, message);
}
}
// Called by AMS, must check permission
- String getSwitchingFromSystemUserMessage() {
- checkHasManageUsersPermission("getSwitchingFromSystemUserMessage()");
+ @Nullable
+ String getSwitchingFromUserMessage(@UserIdInt int userId) {
+ checkHasManageUsersPermission("getSwitchingFromUserMessage()");
- return getSwitchingFromSystemUserMessageUnchecked();
+ return getSwitchingFromUserMessageUnchecked(userId);
}
// Called by AMS, must check permission
- String getSwitchingToSystemUserMessage() {
- checkHasManageUsersPermission("getSwitchingToSystemUserMessage()");
+ @Nullable
+ String getSwitchingToUserMessage(@UserIdInt int userId) {
+ checkHasManageUsersPermission("getSwitchingToUserMessage()");
- return getSwitchingToSystemUserMessageUnchecked();
+ return getSwitchingToUserMessageUnchecked(userId);
}
- private String getSwitchingFromSystemUserMessageUnchecked() {
+ @Nullable
+ private String getSwitchingFromUserMessageUnchecked(@UserIdInt int userId) {
synchronized (mLock) {
- return mSwitchingFromSystemUserMessage;
+ return mSwitchingFromUserMessage.get(userId);
}
}
- private String getSwitchingToSystemUserMessageUnchecked() {
+ @Nullable
+ private String getSwitchingToUserMessageUnchecked(@UserIdInt int userId) {
synchronized (mLock) {
- return mSwitchingToSystemUserMessage;
+ return mSwitchingToUserMessage.get(userId);
}
}
@@ -3518,12 +3522,8 @@ class UserController implements Handler.Callback {
+ mIsBroadcastSentForSystemUserStarted);
pw.println(" mIsBroadcastSentForSystemUserStarting:"
+ mIsBroadcastSentForSystemUserStarting);
- if (mSwitchingFromSystemUserMessage != null) {
- pw.println(" mSwitchingFromSystemUserMessage: " + mSwitchingFromSystemUserMessage);
- }
- if (mSwitchingToSystemUserMessage != null) {
- pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage);
- }
+ pw.println(" mSwitchingFromUserMessage:" + mSwitchingFromUserMessage);
+ pw.println(" mSwitchingToUserMessage:" + mSwitchingToUserMessage);
pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime);
}
}
@@ -4046,7 +4046,7 @@ class UserController implements Handler.Callback {
}
void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
- String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
+ @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage,
@NonNull Runnable onShown) {
if (mService.mContext.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
@@ -4059,7 +4059,7 @@ class UserController implements Handler.Callback {
synchronized (mUserSwitchingDialogLock) {
dismissUserSwitchingDialog(null);
mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser,
- mHandler, switchingFromSystemUserMessage, switchingToSystemUserMessage);
+ mHandler, switchingFromUserMessage, switchingToUserMessage);
mUserSwitchingDialog.show(onShown);
}
}
diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java
index 223e0b79ec0b..f4e733a0c99f 100644
--- a/services/core/java/com/android/server/am/UserSwitchingDialog.java
+++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java
@@ -52,6 +52,7 @@ import com.android.internal.R;
import com.android.internal.util.ObjectUtils;
import com.android.internal.util.UserIcons;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -75,21 +76,23 @@ class UserSwitchingDialog extends Dialog {
protected final UserInfo mOldUser;
protected final UserInfo mNewUser;
- private final String mSwitchingFromSystemUserMessage;
- private final String mSwitchingToSystemUserMessage;
+ @Nullable
+ private final String mSwitchingFromUserMessage;
+ @Nullable
+ private final String mSwitchingToUserMessage;
protected final Context mContext;
private final int mTraceCookie;
UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler,
- String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
+ @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage) {
super(context, R.style.Theme_Material_NoActionBar_Fullscreen);
mContext = context;
mOldUser = oldUser;
mNewUser = newUser;
mHandler = handler;
- mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
- mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ mSwitchingFromUserMessage = switchingFromUserMessage;
+ mSwitchingToUserMessage = switchingToUserMessage;
mDisableAnimations = SystemProperties.getBoolean(
"debug.usercontroller.disable_user_switching_dialog_animations", false);
mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id;
@@ -166,14 +169,14 @@ class UserSwitchingDialog extends Dialog {
: R.string.demo_starting_message);
}
- final String message =
- mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage
- : mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null;
+ if (mSwitchingFromUserMessage != null || mSwitchingToUserMessage != null) {
+ if (mSwitchingFromUserMessage != null && mSwitchingToUserMessage != null) {
+ return mSwitchingFromUserMessage + " " + mSwitchingToUserMessage;
+ }
+ return Objects.requireNonNullElse(mSwitchingFromUserMessage, mSwitchingToUserMessage);
+ }
- return message != null ? message
- // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null,
- // fallback to system message.
- : res.getString(R.string.user_switching_message, mNewUser.name);
+ return res.getString(R.string.user_switching_message, mNewUser.name);
}
@Override
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
index 86f5d9bd637f..c53e4bdc2205 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
@@ -189,11 +189,11 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
@AppOpsManager.HistoricalOpsRequestFilter int requestFilters,
int uidFilter, @Nullable String packageNameFilter,
@Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter,
- long beginTime, long endTime, int limit, String orderByColumn) {
+ long beginTime, long endTime, int limit, String orderByColumn, boolean ascending) {
List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters,
uidFilter, packageNameFilter,
attributionTagFilter, opCodesFilter, opFlagsFilter);
- String sql = buildSql(conditions, orderByColumn, limit);
+ String sql = buildSql(conditions, orderByColumn, ascending, limit);
long startTime = 0;
if (Flags.sqliteDiscreteOpEventLoggingEnabled()) {
startTime = SystemClock.elapsedRealtime();
@@ -249,7 +249,8 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
return results;
}
- private String buildSql(List<SQLCondition> conditions, String orderByColumn, int limit) {
+ private String buildSql(List<SQLCondition> conditions, String orderByColumn, boolean ascending,
+ int limit) {
StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA);
if (!conditions.isEmpty()) {
sql.append(" WHERE ");
@@ -264,6 +265,7 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
if (orderByColumn != null) {
sql.append(" ORDER BY ").append(orderByColumn);
+ sql.append(ascending ? " ASC " : " DESC ");
}
if (limit > 0) {
sql.append(" LIMIT ").append(limit);
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
index c38ee55b4f42..29e78be93da9 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsMigrationHelper.java
@@ -40,7 +40,16 @@ public class DiscreteOpsMigrationHelper {
static void migrateDiscreteOpsToXml(DiscreteOpsSqlRegistry sqlRegistry,
DiscreteOpsXmlRegistry xmlRegistry) {
List<DiscreteOpsSqlRegistry.DiscreteOp> sqlOps = sqlRegistry.getAllDiscreteOps();
- DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(sqlOps);
+
+ // Only migrate configured discrete ops. Sqlite may contain all runtime ops, and more.
+ List<DiscreteOpsSqlRegistry.DiscreteOp> filteredList = new ArrayList<>();
+ for (DiscreteOpsSqlRegistry.DiscreteOp opEvent: sqlOps) {
+ if (DiscreteOpsRegistry.isDiscreteOp(opEvent.getOpCode(), opEvent.getOpFlags())) {
+ filteredList.add(opEvent);
+ }
+ }
+
+ DiscreteOpsXmlRegistry.DiscreteOps xmlOps = getXmlDiscreteOps(filteredList);
xmlRegistry.migrateSqliteData(xmlOps);
sqlRegistry.deleteDatabase();
}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
index b7599f6e40c3..70b7016fbb90 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
@@ -16,6 +16,9 @@
package com.android.server.appop;
+import static android.app.AppOpsManager.OP_ACCESS_ACCESSIBILITY;
+import static android.app.AppOpsManager.OP_ACCESS_NOTIFICATIONS;
+import static android.app.AppOpsManager.OP_BIND_ACCESSIBILITY_SERVICE;
import static android.app.AppOpsManager.OP_CAMERA;
import static android.app.AppOpsManager.OP_COARSE_LOCATION;
import static android.app.AppOpsManager.OP_EMERGENCY_LOCATION;
@@ -23,25 +26,20 @@ import static android.app.AppOpsManager.OP_FINE_LOCATION;
import static android.app.AppOpsManager.OP_FLAG_SELF;
import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY;
+import static android.app.AppOpsManager.OP_GPS;
import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
-import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS;
+import static android.app.AppOpsManager.OP_READ_DEVICE_IDENTIFIERS;
import static android.app.AppOpsManager.OP_READ_HEART_RATE;
-import static android.app.AppOpsManager.OP_READ_ICC_SMS;
import static android.app.AppOpsManager.OP_READ_OXYGEN_SATURATION;
import static android.app.AppOpsManager.OP_READ_SKIN_TEMPERATURE;
-import static android.app.AppOpsManager.OP_READ_SMS;
import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
-import static android.app.AppOpsManager.OP_SEND_SMS;
-import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS;
-import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-import static android.app.AppOpsManager.OP_WRITE_ICC_SMS;
-import static android.app.AppOpsManager.OP_WRITE_SMS;
+import static android.app.AppOpsManager.OP_RUN_IN_BACKGROUND;
import static java.lang.Long.min;
import static java.lang.Math.max;
@@ -51,14 +49,15 @@ import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.os.AsyncTask;
import android.os.Build;
+import android.permission.flags.Flags;
import android.provider.DeviceConfig;
+import android.util.IntArray;
import android.util.Slog;
-import com.android.internal.util.ArrayUtils;
-
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.time.Duration;
+import java.util.Arrays;
import java.util.Date;
import java.util.Set;
@@ -95,21 +94,37 @@ abstract class DiscreteOpsRegistry {
static final String PROPERTY_DISCRETE_HISTORY_QUANTIZATION =
"discrete_history_quantization_millis";
static final String PROPERTY_DISCRETE_FLAGS = "discrete_history_op_flags";
+ // Comma separated app ops list config for testing i.e. "1,2,3,4"
static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist";
- static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
+ // These ops are deemed important for detecting a malicious app, and are recorded.
+ static final int[] IMPORTANT_OPS_FOR_SECURITY = new int[] {
+ OP_GPS,
+ OP_ACCESS_NOTIFICATIONS,
+ OP_RUN_IN_BACKGROUND,
+ OP_BIND_ACCESSIBILITY_SERVICE,
+ OP_ACCESS_ACCESSIBILITY,
+ OP_READ_DEVICE_IDENTIFIERS,
+ OP_MONITOR_HIGH_POWER_LOCATION,
+ OP_MONITOR_LOCATION
+ };
+
+ // These are additional ops, which are not backed by runtime permissions, but are recorded.
+ static final int[] ADDITIONAL_DISCRETE_OPS = new int[] {
+ OP_PHONE_CALL_MICROPHONE,
+ OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+ OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
+ OP_PHONE_CALL_CAMERA,
+ OP_EMERGENCY_LOCATION,
+ OP_RESERVED_FOR_TESTING
+ };
+
+ // Legacy ops captured in discrete database.
+ private static final String LEGACY_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
+ "," + OP_EMERGENCY_LOCATION + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + ","
+ OP_PHONE_CALL_MICROPHONE + "," + OP_PHONE_CALL_CAMERA + ","
+ OP_RECEIVE_AMBIENT_TRIGGER_AUDIO + "," + OP_RECEIVE_SANDBOX_TRIGGER_AUDIO
+ "," + OP_READ_HEART_RATE + "," + OP_READ_OXYGEN_SATURATION + ","
+ OP_READ_SKIN_TEMPERATURE + "," + OP_RESERVED_FOR_TESTING;
- static final int[] sDiscreteOpsToLog =
- new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA,
- OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA,
- OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS,
- OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS,
- OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION,
- OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS,
- };
static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
@@ -121,7 +136,7 @@ abstract class DiscreteOpsRegistry {
// in case of duplicate op events.
static long sDiscreteHistoryQuantization;
- static int[] sDiscreteOps;
+ static int[] sDiscreteOps = new int[0];
static int sDiscreteFlags;
static final int OP_FLAGS_DISCRETE = OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED
@@ -191,7 +206,7 @@ abstract class DiscreteOpsRegistry {
}
static boolean isDiscreteOp(int op, @AppOpsManager.OpFlags int flags) {
- if (!ArrayUtils.contains(sDiscreteOps, op)) {
+ if (Arrays.binarySearch(sDiscreteOps, op) < 0) {
return false;
}
if ((flags & (sDiscreteFlags)) == 0) {
@@ -221,11 +236,42 @@ abstract class DiscreteOpsRegistry {
} else {
sDiscreteHistoryQuantization = DEFAULT_DISCRETE_HISTORY_QUANTIZATION;
}
- sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS) ? sDiscreteFlags =
- p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
- sDiscreteOps = p.getKeyset().contains(PROPERTY_DISCRETE_OPS_LIST) ? parseOpsList(
- p.getString(PROPERTY_DISCRETE_OPS_LIST, DEFAULT_DISCRETE_OPS)) : parseOpsList(
- DEFAULT_DISCRETE_OPS);
+ sDiscreteFlags = p.getKeyset().contains(PROPERTY_DISCRETE_FLAGS)
+ ? p.getInt(PROPERTY_DISCRETE_FLAGS, OP_FLAGS_DISCRETE) : OP_FLAGS_DISCRETE;
+ String opsListConfig = p.getString(PROPERTY_DISCRETE_OPS_LIST, null);
+ sDiscreteOps = opsListConfig == null ? getDefaultOpsList() : parseOpsList(opsListConfig);
+
+ Arrays.sort(sDiscreteOps);
+ }
+
+ // App ops backed by runtime/dangerous permissions.
+ private static IntArray getRuntimePermissionOps() {
+ IntArray runtimeOps = new IntArray();
+ for (int op = 0; op < AppOpsManager._NUM_OP; op++) {
+ if (AppOpsManager.opIsRuntimePermission(op)) {
+ runtimeOps.add(op);
+ }
+ }
+ return runtimeOps;
+ }
+
+ /**
+ * @return an array of app ops captured into discrete database.
+ */
+ private static int[] getDefaultOpsList() {
+ if (!(Flags.recordAllRuntimeAppopsSqlite() && Flags.enableSqliteAppopsAccesses())) {
+ return getDefaultLegacyOps();
+ }
+
+ IntArray discreteOpsArray = getRuntimePermissionOps();
+ discreteOpsArray.addAll(IMPORTANT_OPS_FOR_SECURITY);
+ discreteOpsArray.addAll(ADDITIONAL_DISCRETE_OPS);
+
+ return discreteOpsArray.toArray();
+ }
+
+ private static int[] getDefaultLegacyOps() {
+ return parseOpsList(LEGACY_OPS);
}
private static int[] parseOpsList(String opsList) {
@@ -243,7 +289,7 @@ abstract class DiscreteOpsRegistry {
}
} catch (NumberFormatException e) {
Slog.e(TAG, "Failed to parse Discrete ops list: " + e.getMessage());
- return parseOpsList(DEFAULT_DISCRETE_OPS);
+ return getDefaultOpsList();
}
return result;
}
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index c897891d02c3..0e1fbf3a6d1a 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -180,7 +180,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
ChronoUnit.MILLIS).toEpochMilli());
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis,
- endTimeMillis, -1, null);
+ endTimeMillis, -1, null, false);
LongSparseArray<AttributionChain> attributionChains = null;
if (assembleChains) {
@@ -213,14 +213,15 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
@AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
@NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
int nDiscreteOps) {
- writeAndClearOldAccessHistory();
+ // flush the cache into database before dump.
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
IntArray opCodes = new IntArray();
if (dumpOp != AppOpsManager.OP_NONE) {
opCodes.add(dumpOp);
}
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
packageNameFilter, attributionTagFilter, opCodes, 0, -1,
- -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME);
+ -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME, false);
pw.print(prefix);
pw.print("Largest chain id: ");
diff --git a/services/core/java/com/android/server/display/DisplayControl.java b/services/core/java/com/android/server/display/DisplayControl.java
index ddea285d3564..1d70953de6c0 100644
--- a/services/core/java/com/android/server/display/DisplayControl.java
+++ b/services/core/java/com/android/server/display/DisplayControl.java
@@ -29,7 +29,7 @@ import java.util.Objects;
*/
public class DisplayControl {
private static native IBinder nativeCreateVirtualDisplay(String name, boolean secure,
- String uniqueId, float requestedRefreshRate);
+ boolean optimizeForPower, String uniqueId, float requestedRefreshRate);
private static native void nativeDestroyVirtualDisplay(IBinder displayToken);
private static native void nativeOverrideHdrTypes(IBinder displayToken, int[] modes);
private static native long[] nativeGetPhysicalDisplayIds();
@@ -49,7 +49,7 @@ public class DisplayControl {
*/
public static IBinder createVirtualDisplay(String name, boolean secure) {
Objects.requireNonNull(name, "name must not be null");
- return nativeCreateVirtualDisplay(name, secure, "", 0.0f);
+ return nativeCreateVirtualDisplay(name, secure, true, "", 0.0f);
}
/**
@@ -57,6 +57,10 @@ public class DisplayControl {
*
* @param name The name of the virtual display.
* @param secure Whether this display is secure.
+ * @param optimizeForPower Whether SurfaceFlinger should optimize for power (instead of
+ * performance). Such displays will depend on another display for it to
+ * be shown and rendered, and that display will optimize for
+ * performance when it is on.
* @param uniqueId The unique ID for the display.
* @param requestedRefreshRate The requested refresh rate in frames per second.
* For best results, specify a divisor of the physical refresh rate, e.g., 30 or 60 on
@@ -66,10 +70,11 @@ public class DisplayControl {
* @return The token reference for the display in SurfaceFlinger.
*/
public static IBinder createVirtualDisplay(String name, boolean secure,
- String uniqueId, float requestedRefreshRate) {
+ boolean optimizeForPower, String uniqueId, float requestedRefreshRate) {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(uniqueId, "uniqueId must not be null");
- return nativeCreateVirtualDisplay(name, secure, uniqueId, requestedRefreshRate);
+ return nativeCreateVirtualDisplay(name, secure, optimizeForPower, uniqueId,
+ requestedRefreshRate);
}
/**
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index ac03a93ca9e1..c2eac8605851 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -103,10 +103,10 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
Context context, Handler handler, Listener listener, DisplayManagerFlags featureFlags) {
this(syncRoot, context, handler, listener, new SurfaceControlDisplayFactory() {
@Override
- public IBinder createDisplay(String name, boolean secure, String uniqueId,
- float requestedRefreshRate) {
- return DisplayControl.createVirtualDisplay(name, secure, uniqueId,
- requestedRefreshRate);
+ public IBinder createDisplay(String name, boolean secure, boolean optimizeForPower,
+ String uniqueId, float requestedRefreshRate) {
+ return DisplayControl.createVirtualDisplay(name, secure, optimizeForPower, uniqueId,
+ requestedRefreshRate);
}
@Override
@@ -182,9 +182,13 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
String name = virtualDisplayConfig.getName();
boolean secure = (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0;
+ boolean neverBlank = isNeverBlank(flags);
- IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, uniqueId,
- virtualDisplayConfig.getRequestedRefreshRate());
+ // Never-blank displays are considered to be dependent on another display to be rendered.
+ // As a result, such displays should optimize for power instead of performance when it is
+ // powered on.
+ IBinder displayToken = mSurfaceControlDisplayFactory.createDisplay(name, secure, neverBlank,
+ uniqueId, virtualDisplayConfig.getRequestedRefreshRate());
MediaProjectionCallback mediaProjectionCallback = null;
if (projection != null) {
mediaProjectionCallback = new MediaProjectionCallback(appToken);
@@ -318,6 +322,12 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
return mVirtualDisplayDevices.remove(appToken);
}
+ private static boolean isNeverBlank(int flags) {
+ // Private non-mirror displays are never blank and always on.
+ return (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
+ && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0;
+ }
+
private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient {
private static final int PENDING_SURFACE_CHANGE = 0x01;
private static final int PENDING_RESIZE = 0x02;
@@ -377,9 +387,7 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
mCallback = callback;
mProjection = projection;
mMediaProjectionCallback = mediaProjectionCallback;
- // Private non-mirror displays are never blank and always on.
- mNeverBlank = (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
- && (flags & VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0;
+ mNeverBlank = isNeverBlank(flags);
if (android.companion.virtualdevice.flags.Flags.correctVirtualDisplayPowerState()
&& !mNeverBlank) {
// The display's power state depends on the power state of the state of its
@@ -782,6 +790,10 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
*
* @param name The name of the display.
* @param secure Whether this display is secure.
+ * @param optimizeForPower Whether SurfaceFlinger should optimize for power (instead of
+ * performance). Such displays will depend on another display for
+ * it to be shown and rendered, and that display will optimize for
+ * performance when it is on.
* @param uniqueId The unique ID for the display.
* @param requestedRefreshRate
* The refresh rate, frames per second, to request on the virtual display.
@@ -791,8 +803,8 @@ public class VirtualDisplayAdapter extends DisplayAdapter {
* the refresh rate of the leader physical display.
* @return The token reference for the display in SurfaceFlinger.
*/
- IBinder createDisplay(String name, boolean secure, String uniqueId,
- float requestedRefreshRate);
+ IBinder createDisplay(String name, boolean secure, boolean optimizeForPower,
+ String uniqueId, float requestedRefreshRate);
/**
* Destroy a display in SurfaceFlinger.
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index ee29849465e2..737d943f084d 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -308,7 +308,9 @@ final class OverlayManagerServiceImpl {
Slog.d(TAG, "onPackageRemoved pkgName=" + pkgName + " userId=" + userId);
}
// Update the state of all overlays that target this package.
- final Set<UserPackage> targets = updateOverlaysForTarget(pkgName, userId, 0 /* flags */);
+ Set<UserPackage> targets = Collections.emptySet();
+ targets = CollectionUtils.addAll(targets,
+ updateOverlaysForTarget(pkgName, userId, 0 /* flags */));
// Remove all the overlays this package declares.
return CollectionUtils.addAll(targets,
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 734920435e26..3361dbc2df07 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -644,20 +644,6 @@ final class InstallRequest {
return mScanResult.mPkgSetting;
}
- @Nullable
- public PackageSetting getRealPackageSetting() {
- // TODO: Fix this to have 1 mutable PackageSetting for scan/install. If the previous
- // setting needs to be passed to have a comparison, hide it behind an immutable
- // interface. There's no good reason to have 3 different ways to access the real
- // PackageSetting object, only one of which is actually correct.
- PackageSetting realPkgSetting = isExistingSettingCopied()
- ? getScanRequestPackageSetting() : getScannedPackageSetting();
- if (realPkgSetting == null) {
- realPkgSetting = getScannedPackageSetting();
- }
- return realPkgSetting;
- }
-
public boolean isExistingSettingCopied() {
assertScanResultExists();
return mScanResult.mExistingSettingCopied;
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 58c5b1c90a66..5798aa919d96 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -36,6 +36,7 @@ import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_COMMUNAL;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
+import static android.os.UserManager.USER_TYPE_PROFILE_SUPERVISING;
import static android.os.UserManager.USER_TYPE_PROFILE_TEST;
import static android.os.UserManager.USER_TYPE_SYSTEM_HEADLESS;
@@ -111,6 +112,7 @@ public final class UserTypeFactory {
builders.put(USER_TYPE_PROFILE_CLONE, getDefaultTypeProfileClone());
builders.put(USER_TYPE_PROFILE_COMMUNAL, getDefaultTypeProfileCommunal());
builders.put(USER_TYPE_PROFILE_PRIVATE, getDefaultTypeProfilePrivate());
+ builders.put(USER_TYPE_PROFILE_SUPERVISING, getDefaultTypeProfileSupervising());
if (Build.IS_DEBUGGABLE) {
builders.put(USER_TYPE_PROFILE_TEST, getDefaultTypeProfileTest());
}
@@ -343,6 +345,29 @@ public final class UserTypeFactory {
}
/**
+ * Returns the Builder for the default {@link UserManager#USER_TYPE_PROFILE_SUPERVISING}
+ * configuration.
+ */
+ private static UserTypeDetails.Builder getDefaultTypeProfileSupervising() {
+ return new UserTypeDetails.Builder()
+ .setName(USER_TYPE_PROFILE_SUPERVISING)
+ .setBaseType(FLAG_PROFILE)
+ .setMaxAllowed(1)
+ .setProfileParentRequired(false)
+ .setEnabled(android.multiuser.Flags.allowSupervisingProfile() ? 1 : 0)
+ .setLabels(R.string.profile_label_supervising)
+ .setDefaultRestrictions(getDefaultSupervisingProfileRestrictions())
+ .setDefaultSecureSettings(getDefaultNonManagedProfileSecureSettings())
+ .setDefaultUserProperties(new UserProperties.Builder()
+ .setStartWithParent(false)
+ .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_NO)
+ .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_NO)
+ .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+ .setCredentialShareableWithParent(false)
+ .setAlwaysVisible(true));
+ }
+
+ /**
* Returns the Builder for the default {@link UserManager#USER_TYPE_FULL_SECONDARY}
* configuration.
*/
@@ -449,6 +474,12 @@ public final class UserTypeFactory {
return restrictions;
}
+ private static Bundle getDefaultSupervisingProfileRestrictions() {
+ final Bundle restrictions = getDefaultProfileRestrictions();
+ restrictions.putBoolean(UserManager.DISALLOW_INSTALL_APPS, true);
+ return restrictions;
+ }
+
private static Bundle getDefaultManagedProfileSecureSettings() {
// Only add String values to the bundle, settings are written as Strings eventually
final Bundle settings = new Bundle();
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
index 54365ff03db0..c5a43a57da82 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
@@ -72,14 +72,20 @@ class SelinuxAuditLogsCollector {
}
SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
- this(
- () ->
- DeviceConfig.getString(
- DeviceConfig.NAMESPACE_ADSERVICES,
- CONFIG_SELINUX_AUDIT_DOMAIN,
- DEFAULT_SELINUX_AUDIT_DOMAIN),
- rateLimiter,
- quotaLimiter);
+ this(new DefaultDomainSupplier(), rateLimiter, quotaLimiter);
+ }
+
+ private static class DefaultDomainSupplier implements Supplier<String> {
+ @Override
+ public String get() {
+ if (SelinuxAuditLogsService.enabledForAllDomains()) {
+ return "\\w+";
+ }
+ return DeviceConfig.getString(
+ DeviceConfig.NAMESPACE_ADSERVICES,
+ CONFIG_SELINUX_AUDIT_DOMAIN,
+ DEFAULT_SELINUX_AUDIT_DOMAIN);
+ }
}
public void setStopRequested(boolean stopRequested) {
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
index d46e8916d9e9..9dc457c5d63b 100644
--- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
@@ -16,6 +16,7 @@
package com.android.server.selinux;
import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit;
+import static com.android.server.selinux.flags.Flags.selinuxLogsCollect;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -49,6 +50,9 @@ public class SelinuxAuditLogsService extends JobService {
"selinux_audit_job_frequency_hours";
private static final String CONFIG_SELINUX_ENABLE_AUDIT_JOB = "selinux_enable_audit_job";
private static final String CONFIG_SELINUX_AUDIT_CAP = "selinux_audit_cap";
+ private static final String DEVICE_CONFIG_SECURITY_NAMESPACE = "security";
+ private static final String CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED =
+ "selinux_audit_job_enabled";
private static final int MAX_PERMITS_CAP_DEFAULT = 50000;
private static final int SELINUX_AUDIT_JOB_ID = 25327386;
@@ -76,7 +80,7 @@ public class SelinuxAuditLogsService extends JobService {
/** Schedule jobs with the {@link JobScheduler}. */
public static void schedule(Context context) {
- if (!selinuxSdkSandboxAudit()) {
+ if (!selinuxSdkSandboxAudit() && !enabledForAllDomains()) {
Slog.d(TAG, "SelinuxAuditLogsService not enabled");
return;
}
@@ -86,13 +90,20 @@ public class SelinuxAuditLogsService extends JobService {
return;
}
- LogsCollectorJobScheduler propertiesListener =
+ LogsCollectorJobScheduler scheduler =
new LogsCollectorJobScheduler(
context.getSystemService(JobScheduler.class)
.forNamespace(SELINUX_AUDIT_NAMESPACE));
- propertiesListener.schedule();
+ scheduler.schedule();
+
+ AdServicesPropertyMonitor adServicesProperties = new AdServicesPropertyMonitor(scheduler);
+ DeviceConfig.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), adServicesProperties);
+
+ SecurityPropertyMonitor securityProperties = new SecurityPropertyMonitor(scheduler);
DeviceConfig.addOnPropertiesChangedListener(
- DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), propertiesListener);
+ DEVICE_CONFIG_SECURITY_NAMESPACE, context.getMainExecutor(), securityProperties);
+
}
@Override
@@ -101,7 +112,7 @@ public class SelinuxAuditLogsService extends JobService {
Slog.e(TAG, "The job id does not match the expected selinux job id.");
return false;
}
- if (!selinuxSdkSandboxAudit()) {
+ if (!selinuxSdkSandboxAudit() && !enabledForAllDomains()) {
Slog.i(TAG, "Selinux audit job disabled.");
return false;
}
@@ -123,17 +134,33 @@ public class SelinuxAuditLogsService extends JobService {
return false;
}
- /**
- * This class is in charge of scheduling the job service, and keeping the scheduling up to date
- * when the parameters change.
- */
- private static final class LogsCollectorJobScheduler
+ /** Checks if the service is enabled for all domains */
+ public static final boolean enabledForAllDomains() {
+ if (selinuxLogsCollect()) {
+ return DeviceConfig.getBoolean(
+ DEVICE_CONFIG_SECURITY_NAMESPACE,
+ CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED,
+ false);
+ }
+ return false;
+ }
+
+ /** Checks if the service is enabled for SDK Sandbox */
+ public static final boolean enabledForSdkSandbox() {
+ if (selinuxSdkSandboxAudit()) {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ADSERVICES, CONFIG_SELINUX_ENABLE_AUDIT_JOB, false);
+ }
+ return false;
+ }
+
+ private static final class AdServicesPropertyMonitor
implements DeviceConfig.OnPropertiesChangedListener {
- private final JobScheduler mJobScheduler;
+ private final LogsCollectorJobScheduler mScheduler;
- private LogsCollectorJobScheduler(JobScheduler jobScheduler) {
- mJobScheduler = jobScheduler;
+ private AdServicesPropertyMonitor(LogsCollectorJobScheduler scheduler) {
+ mScheduler = scheduler;
}
@Override
@@ -149,19 +176,65 @@ public class SelinuxAuditLogsService extends JobService {
if (keyset.contains(CONFIG_SELINUX_ENABLE_AUDIT_JOB)) {
boolean enabled =
changedProperties.getBoolean(
- CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false);
+ CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false)
+ || enabledForAllDomains();
if (enabled) {
- schedule();
+ mScheduler.schedule();
} else {
- mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID);
+ mScheduler.cancel();
}
} else if (keyset.contains(CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS)) {
// The job frequency changed, reschedule.
- schedule();
+ mScheduler.schedule();
}
}
+ }
+
+ private static final class SecurityPropertyMonitor
+ implements DeviceConfig.OnPropertiesChangedListener {
+
+ private final LogsCollectorJobScheduler mScheduler;
+
+ private SecurityPropertyMonitor(LogsCollectorJobScheduler scheduler) {
+ mScheduler = scheduler;
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties changedProperties) {
+ Set<String> keyset = changedProperties.getKeyset();
+
+ if (keyset.contains(CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED)) {
+ boolean enabled =
+ changedProperties.getBoolean(
+ CONFIG_SECURITY_SELINUX_AUDIT_JOB_ENABLED,
+ /* defaultValue= */ false)
+ || enabledForSdkSandbox();
+ if (enabled) {
+ mScheduler.schedule();
+ } else {
+ mScheduler.cancel();
+ }
+ }
+ }
+ }
+
+ /**
+ * This class is in charge of scheduling the job service, and keeping the scheduling up to date
+ * when the parameters change.
+ */
+ private static final class LogsCollectorJobScheduler {
+
+ private final JobScheduler mJobScheduler;
+
+ private LogsCollectorJobScheduler(JobScheduler jobScheduler) {
+ mJobScheduler = jobScheduler;
+ }
+
+ public void cancel() {
+ mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID);
+ }
- private void schedule() {
+ public void schedule() {
long frequencyMillis =
TimeUnit.HOURS.toMillis(
DeviceConfig.getInt(
diff --git a/services/core/java/com/android/server/selinux/flags.aconfig b/services/core/java/com/android/server/selinux/flags.aconfig
new file mode 100644
index 000000000000..3bb5a6bda1de
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.selinux.flags"
+container: "system"
+
+flag {
+ name: "selinux_logs_collect"
+ namespace: "network_security"
+ description: "Enable collection of SELinux denials based on selinux_audit_job_enabled"
+ bug: "372950125"
+}
diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
index 31348cd9156f..17980c02502f 100644
--- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
+++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
@@ -177,6 +177,7 @@ public final class TextClassificationManagerService extends ITextClassifierServi
private final String mDefaultTextClassifierPackage;
@Nullable
private final String mSystemTextClassifierPackage;
+ private final MyPackageMonitor mPackageMonitor;
private TextClassificationManagerService(Context context) {
mContext = Objects.requireNonNull(context);
@@ -187,50 +188,50 @@ public final class TextClassificationManagerService extends ITextClassifierServi
mDefaultTextClassifierPackage = packageManager.getDefaultTextClassifierPackageName();
mSystemTextClassifierPackage = packageManager.getSystemTextClassifierPackageName();
mSessionCache = new SessionCache(mLock);
+ mPackageMonitor = new MyPackageMonitor();
}
private void startListenSettings() {
mSettingsListener.registerObserver();
}
- void startTrackingPackageChanges() {
- final PackageMonitor monitor = new PackageMonitor() {
-
- @Override
- public void onPackageAdded(String packageName, int uid) {
- notifyPackageInstallStatusChange(packageName, /* installed*/ true);
- }
+ private class MyPackageMonitor extends PackageMonitor {
+ @Override
+ public void onPackageAdded(String packageName, int uid) {
+ notifyPackageInstallStatusChange(packageName, /* installed*/ true);
+ }
- @Override
- public void onPackageRemoved(String packageName, int uid) {
- notifyPackageInstallStatusChange(packageName, /* installed= */ false);
- }
+ @Override
+ public void onPackageRemoved(String packageName, int uid) {
+ notifyPackageInstallStatusChange(packageName, /* installed= */ false);
+ }
- @Override
- public void onPackageModified(String packageName) {
- final int userId = getChangingUserId();
- synchronized (mLock) {
- final UserState userState = getUserStateLocked(userId);
- final ServiceState serviceState = userState.getServiceStateLocked(packageName);
- if (serviceState != null) {
- serviceState.onPackageModifiedLocked();
- }
+ @Override
+ public void onPackageModified(String packageName) {
+ final int userId = getChangingUserId();
+ synchronized (mLock) {
+ final UserState userState = getUserStateLocked(userId);
+ final ServiceState serviceState = userState.getServiceStateLocked(packageName);
+ if (serviceState != null) {
+ serviceState.onPackageModifiedLocked();
}
}
+ }
- private void notifyPackageInstallStatusChange(String packageName, boolean installed) {
- final int userId = getChangingUserId();
- synchronized (mLock) {
- final UserState userState = getUserStateLocked(userId);
- final ServiceState serviceState = userState.getServiceStateLocked(packageName);
- if (serviceState != null) {
- serviceState.onPackageInstallStatusChangeLocked(installed);
- }
+ private void notifyPackageInstallStatusChange(String packageName, boolean installed) {
+ final int userId = getChangingUserId();
+ synchronized (mLock) {
+ final UserState userState = getUserStateLocked(userId);
+ final ServiceState serviceState = userState.getServiceStateLocked(packageName);
+ if (serviceState != null) {
+ serviceState.onPackageInstallStatusChangeLocked(installed);
}
}
- };
+ }
+ }
- monitor.register(mContext, null, UserHandle.ALL, true);
+ void startTrackingPackageChanges() {
+ mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
}
@Override
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 8bcf1a9be031..47d6879129ee 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -183,7 +183,7 @@ public final class TvInputManagerService extends SystemService {
private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>();
private final MessageHandler mMessageHandler;
-
+ private final MyPackageMonitor mPackageMonitor;
private final ActivityManager mActivityManager;
private boolean mExternalInputLoggingDisplayNameFilterEnabled = false;
@@ -200,6 +200,7 @@ public final class TvInputManagerService extends SystemService {
mMessageHandler =
new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper());
mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
+ mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true);
mActivityManager =
(ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
@@ -298,74 +299,79 @@ public final class TvInputManagerService extends SystemService {
mExternalInputLoggingDeviceBrandNames.addAll(Arrays.asList(deviceBrandNames));
}
- private void registerBroadcastReceivers() {
- PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) {
- private void buildTvInputList(String[] packages) {
- int userId = getChangingUserId();
- synchronized (mLock) {
- if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
- buildTvInputListLocked(userId, packages);
- buildTvContentRatingSystemListLocked(userId);
- }
+ private class MyPackageMonitor extends PackageMonitor {
+ MyPackageMonitor(boolean supportsPackageRestartQuery) {
+ super(supportsPackageRestartQuery);
+ }
+
+ private void buildTvInputList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvInputListLocked(userId, packages);
+ buildTvContentRatingSystemListLocked(userId);
}
}
+ }
- @Override
- public void onPackageUpdateFinished(String packageName, int uid) {
- if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
- // This callback is invoked when the TV input is reinstalled.
- // In this case, isReplacing() always returns true.
- buildTvInputList(new String[] { packageName });
- }
+ @Override
+ public void onPackageUpdateFinished(String packageName, int uid) {
+ if (DEBUG) Slog.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
+ // This callback is invoked when the TV input is reinstalled.
+ // In this case, isReplacing() always returns true.
+ buildTvInputList(new String[] { packageName });
+ }
- @Override
- public void onPackagesAvailable(String[] packages) {
- if (DEBUG) {
- Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
- }
- // This callback is invoked when the media on which some packages exist become
- // available.
- if (isReplacing()) {
- buildTvInputList(packages);
- }
+ @Override
+ public void onPackagesAvailable(String[] packages) {
+ if (DEBUG) {
+ Slog.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
}
+ // This callback is invoked when the media on which some packages exist become
+ // available.
+ if (isReplacing()) {
+ buildTvInputList(packages);
+ }
+ }
- @Override
- public void onPackagesUnavailable(String[] packages) {
- // This callback is invoked when the media on which some packages exist become
- // unavailable.
- if (DEBUG) {
- Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
- + ")");
- }
- if (isReplacing()) {
- buildTvInputList(packages);
- }
+ @Override
+ public void onPackagesUnavailable(String[] packages) {
+ // This callback is invoked when the media on which some packages exist become
+ // unavailable.
+ if (DEBUG) {
+ Slog.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
+ + ")");
}
+ if (isReplacing()) {
+ buildTvInputList(packages);
+ }
+ }
- @Override
- public void onSomePackagesChanged() {
- // TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage
- // the TV inputs.
- if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()");
- if (isReplacing()) {
- if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing");
- // When the package is updated, buildTvInputListLocked is called in other
- // methods instead.
- return;
- }
- buildTvInputList(null);
+ @Override
+ public void onSomePackagesChanged() {
+ // TODO: Use finer-grained methods(e.g. onPackageAdded, onPackageRemoved) to manage
+ // the TV inputs.
+ if (DEBUG) Slog.d(TAG, "onSomePackagesChanged()");
+ if (isReplacing()) {
+ if (DEBUG) Slog.d(TAG, "Skipped building TV input list due to replacing");
+ // When the package is updated, buildTvInputListLocked is called in other
+ // methods instead.
+ return;
}
+ buildTvInputList(null);
+ }
- @Override
- public boolean onPackageChanged(String packageName, int uid, String[] components) {
- // The input list needs to be updated in any cases, regardless of whether
- // it happened to the whole package or a specific component. Returning true so that
- // the update can be handled in {@link #onSomePackagesChanged}.
- return true;
- }
- };
- monitor.register(mContext, null, UserHandle.ALL, true);
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // The input list needs to be updated in any cases, regardless of whether
+ // it happened to the whole package or a specific component. Returning true so that
+ // the update can be handled in {@link #onSomePackagesChanged}.
+ return true;
+ }
+ }
+
+ private void registerBroadcastReceivers() {
+ mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 6a7fc6dcf7cd..42013fab7a14 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -105,6 +105,7 @@ public class TvInteractiveAppManagerService extends SystemService {
// A global lock.
private final Object mLock = new Object();
private final Context mContext;
+ private final MyPackageMonitor mPackageMonitor;
// ID of the current user.
@GuardedBy("mLock")
private int mCurrentUserId = UserHandle.USER_SYSTEM;
@@ -138,6 +139,7 @@ public class TvInteractiveAppManagerService extends SystemService {
super(context);
mContext = context;
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
+ mPackageMonitor = new MyPackageMonitor(/* supportsPackageRestartQuery */ true);
}
@GuardedBy("mLock")
@@ -518,86 +520,91 @@ public class TvInteractiveAppManagerService extends SystemService {
}
}
- private void registerBroadcastReceivers() {
- PackageMonitor monitor = new PackageMonitor(/* supportsPackageRestartQuery */ true) {
- private void buildTvInteractiveAppServiceList(String[] packages) {
- int userId = getChangingUserId();
- synchronized (mLock) {
- if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
- buildTvInteractiveAppServiceListLocked(userId, packages);
- buildAppLinkInfoLocked(userId);
- }
+ private class MyPackageMonitor extends PackageMonitor {
+ MyPackageMonitor(boolean supportsPackageRestartQuery) {
+ super(supportsPackageRestartQuery);
+ }
+
+ private void buildTvInteractiveAppServiceList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvInteractiveAppServiceListLocked(userId, packages);
+ buildAppLinkInfoLocked(userId);
}
}
- private void buildTvAdServiceList(String[] packages) {
- int userId = getChangingUserId();
- synchronized (mLock) {
- if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
- buildTvAdServiceListLocked(userId, packages);
- }
+ }
+ private void buildTvAdServiceList(String[] packages) {
+ int userId = getChangingUserId();
+ synchronized (mLock) {
+ if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) {
+ buildTvAdServiceListLocked(userId, packages);
}
}
+ }
- @Override
- public void onPackageUpdateFinished(String packageName, int uid) {
- if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
- // This callback is invoked when the TV interactive App service is reinstalled.
- // In this case, isReplacing() always returns true.
- buildTvInteractiveAppServiceList(new String[] { packageName });
- buildTvAdServiceList(new String[] { packageName });
- }
+ @Override
+ public void onPackageUpdateFinished(String packageName, int uid) {
+ if (DEBUG) Slogf.d(TAG, "onPackageUpdateFinished(packageName=" + packageName + ")");
+ // This callback is invoked when the TV interactive App service is reinstalled.
+ // In this case, isReplacing() always returns true.
+ buildTvInteractiveAppServiceList(new String[] { packageName });
+ buildTvAdServiceList(new String[] { packageName });
+ }
- @Override
- public void onPackagesAvailable(String[] packages) {
- if (DEBUG) {
- Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
- }
- // This callback is invoked when the media on which some packages exist become
- // available.
- if (isReplacing()) {
- buildTvInteractiveAppServiceList(packages);
- buildTvAdServiceList(packages);
- }
+ @Override
+ public void onPackagesAvailable(String[] packages) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onPackagesAvailable(packages=" + Arrays.toString(packages) + ")");
}
+ // This callback is invoked when the media on which some packages exist become
+ // available.
+ if (isReplacing()) {
+ buildTvInteractiveAppServiceList(packages);
+ buildTvAdServiceList(packages);
+ }
+ }
- @Override
- public void onPackagesUnavailable(String[] packages) {
- // This callback is invoked when the media on which some packages exist become
- // unavailable.
- if (DEBUG) {
- Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
- + ")");
- }
- if (isReplacing()) {
- buildTvInteractiveAppServiceList(packages);
- buildTvAdServiceList(packages);
- }
+ @Override
+ public void onPackagesUnavailable(String[] packages) {
+ // This callback is invoked when the media on which some packages exist become
+ // unavailable.
+ if (DEBUG) {
+ Slogf.d(TAG, "onPackagesUnavailable(packages=" + Arrays.toString(packages)
+ + ")");
}
+ if (isReplacing()) {
+ buildTvInteractiveAppServiceList(packages);
+ buildTvAdServiceList(packages);
+ }
+ }
- @Override
- public void onSomePackagesChanged() {
- if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()");
- if (isReplacing()) {
- if (DEBUG) {
- Slogf.d(TAG, "Skipped building TV interactive App list due to replacing");
- }
- // When the package is updated, buildTvInteractiveAppServiceListLocked is called
- // in other methods instead.
- return;
+ @Override
+ public void onSomePackagesChanged() {
+ if (DEBUG) Slogf.d(TAG, "onSomePackagesChanged()");
+ if (isReplacing()) {
+ if (DEBUG) {
+ Slogf.d(TAG, "Skipped building TV interactive App list due to replacing");
}
- buildTvInteractiveAppServiceList(null);
- buildTvAdServiceList(null);
+ // When the package is updated, buildTvInteractiveAppServiceListLocked is called
+ // in other methods instead.
+ return;
}
+ buildTvInteractiveAppServiceList(null);
+ buildTvAdServiceList(null);
+ }
- @Override
- public boolean onPackageChanged(String packageName, int uid, String[] components) {
- // The interactive App list needs to be updated in any cases, regardless of whether
- // it happened to the whole package or a specific component. Returning true so that
- // the update can be handled in {@link #onSomePackagesChanged}.
- return true;
- }
- };
- monitor.register(mContext, null, UserHandle.ALL, true);
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // The interactive App list needs to be updated in any cases, regardless of whether
+ // it happened to the whole package or a specific component. Returning true so that
+ // the update can be handled in {@link #onSomePackagesChanged}.
+ return true;
+ }
+ }
+
+ private void registerBroadcastReceivers() {
+ mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_SWITCHED);
diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
index c8e7a8dea5c3..250e99b47b1a 100644
--- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
@@ -19,10 +19,7 @@ package com.android.server.updates;
import android.content.Context;
import android.content.Intent;
-import java.io.File;
-
public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
- private static final String KEYCHAIN_DIR = "/data/misc/keychain/";
public CertPinInstallReceiver() {
super("/data/misc/keychain/", "pins", "metadata/", "version");
@@ -30,22 +27,7 @@ public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
- if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- if (com.android.server.flags.Flags.certpininstallerRemoval()) {
- File pins = new File(KEYCHAIN_DIR + "pins");
- if (pins.exists()) {
- pins.delete();
- }
- File version = new File(KEYCHAIN_DIR + "metadata/version");
- if (version.exists()) {
- version.delete();
- }
- File metadata = new File(KEYCHAIN_DIR + "metadata");
- if (metadata.exists()) {
- metadata.delete();
- }
- }
- } else if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
+ if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
super.onReceive(context, intent);
}
}
diff --git a/services/core/java/com/android/server/vr/EnabledComponentsObserver.java b/services/core/java/com/android/server/vr/EnabledComponentsObserver.java
index 7c2ce6467122..458cb023cc4e 100644
--- a/services/core/java/com/android/server/vr/EnabledComponentsObserver.java
+++ b/services/core/java/com/android/server/vr/EnabledComponentsObserver.java
@@ -58,6 +58,7 @@ public class EnabledComponentsObserver implements SettingChangeListener {
private final Object mLock;
private final Context mContext;
+ private final PackageMonitor mPackageMonitor;
private final String mSettingName;
private final String mServiceName;
private final String mServicePermission;
@@ -78,13 +79,39 @@ public class EnabledComponentsObserver implements SettingChangeListener {
private EnabledComponentsObserver(@NonNull Context context, @NonNull String settingName,
@NonNull String servicePermission, @NonNull String serviceName, @NonNull Object lock,
- @NonNull Collection<EnabledComponentChangeListener> listeners) {
+ @NonNull Collection<EnabledComponentChangeListener> listeners,
+ @NonNull Looper looper) {
mLock = lock;
mContext = context;
mSettingName = settingName;
mServiceName = serviceName;
mServicePermission = servicePermission;
mEnabledComponentListeners.addAll(listeners);
+ mPackageMonitor = new PackageMonitor(true) {
+ @Override
+ public void onSomePackagesChanged() {
+ onPackagesChanged();
+ }
+
+ @Override
+ public void onPackageDisappeared(String packageName, int reason) {
+ onPackagesChanged();
+ }
+
+ @Override
+ public void onPackageModified(String packageName) {
+ onPackagesChanged();
+ }
+
+ @Override
+ public boolean onHandleForceStop(Intent intent, String[] packages, int uid,
+ boolean doit) {
+ onPackagesChanged();
+ return super.onHandleForceStop(intent, packages, uid, doit);
+ }
+ };
+
+ mPackageMonitor.register(context, looper, UserHandle.ALL, true);;
}
/**
@@ -108,38 +135,7 @@ public class EnabledComponentsObserver implements SettingChangeListener {
SettingsObserver s = SettingsObserver.build(context, handler, settingName);
final EnabledComponentsObserver o = new EnabledComponentsObserver(context, settingName,
- servicePermission, serviceName, lock, listeners);
-
- PackageMonitor packageMonitor = new PackageMonitor(true) {
- @Override
- public void onSomePackagesChanged() {
- o.onPackagesChanged();
-
- }
-
- @Override
- public void onPackageDisappeared(String packageName, int reason) {
- o.onPackagesChanged();
-
- }
-
- @Override
- public void onPackageModified(String packageName) {
- o.onPackagesChanged();
-
- }
-
- @Override
- public boolean onHandleForceStop(Intent intent, String[] packages, int uid,
- boolean doit) {
- o.onPackagesChanged();
-
- return super.onHandleForceStop(intent, packages, uid, doit);
- }
- };
-
- packageMonitor.register(context, looper, UserHandle.ALL, true);
-
+ servicePermission, serviceName, lock, listeners, looper);
s.addListener(o);
return o;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 8e8455ad5288..6e640d890fb8 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -17,7 +17,6 @@
package com.android.server.wallpaper;
import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
-import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE;
import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static android.app.WallpaperManager.getOrientation;
import static android.app.WallpaperManager.getRotatedOrientation;
@@ -85,20 +84,11 @@ public class WallpaperCropper {
private final WallpaperDisplayHelper mWallpaperDisplayHelper;
- /**
- * Helpers exposed to the window manager part (WallpaperController)
- */
- public interface WallpaperCropUtils {
-
- /**
- * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}
- */
- Rect getCrop(Point displaySize, Point bitmapSize,
- SparseArray<Rect> suggestedCrops, boolean rtl);
- }
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) {
mWallpaperDisplayHelper = wallpaperDisplayHelper;
+ mDefaultDisplayInfo = mWallpaperDisplayHelper.getDefaultDisplayInfo();
}
/**
@@ -116,16 +106,16 @@ public class WallpaperCropper {
* {@link #getAdjustedCrop}.
* </ul>
*
- * @param displaySize The dimensions of the surface where we want to render the wallpaper
- * @param bitmapSize The dimensions of the wallpaper bitmap
- * @param rtl Whether the device is right-to-left
- * @param suggestedCrops An optional list of user-defined crops for some orientations.
- * If there is a suggested crop for
+ * @param displaySize The dimensions of the surface where we want to render the wallpaper
+ * @param defaultDisplayInfo The default display info
+ * @param bitmapSize The dimensions of the wallpaper bitmap
+ * @param rtl Whether the device is right-to-left
+ * @param suggestedCrops An optional list of user-defined crops for some orientations.
*
* @return A Rect indicating how to crop the bitmap for the current display.
*/
- public Rect getCrop(Point displaySize, Point bitmapSize,
- SparseArray<Rect> suggestedCrops, boolean rtl) {
+ public static Rect getCrop(Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo,
+ Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) {
int orientation = getOrientation(displaySize);
@@ -135,23 +125,24 @@ public class WallpaperCropper {
// The first exception is if the device is a foldable and we're on the folded screen.
// In that case, show the center of what's on the unfolded screen.
- int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation);
if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
// Let the system know that we're showing the full image on the unfolded screen
SparseArray<Rect> newSuggestedCrops = new SparseArray<>();
newSuggestedCrops.put(unfoldedOrientation, crop);
// This will fall into "Case 4" of this function and center the folded screen
- return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, newSuggestedCrops,
+ rtl);
}
// The second exception is if we're on tablet and we're on portrait mode.
// In that case, center the wallpaper relatively to landscape and put some parallax.
- boolean isTablet = mWallpaperDisplayHelper.isLargeScreen()
- && !mWallpaperDisplayHelper.isFoldable();
+ boolean isTablet = defaultDisplayInfo.isLargeScreen && !defaultDisplayInfo.isFoldable;
if (isTablet && displaySize.x < displaySize.y) {
Point rotatedDisplaySize = new Point(displaySize.y, displaySize.x);
// compute the crop on landscape (without parallax)
- Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ Rect landscapeCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl);
landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl);
// compute the crop on portrait at the center of the landscape crop
crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD);
@@ -173,7 +164,8 @@ public class WallpaperCropper {
if (testCrop == null || testCrop.left < 0 || testCrop.top < 0
|| testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) {
Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize);
- return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(),
+ rtl);
}
}
@@ -185,10 +177,9 @@ public class WallpaperCropper {
// Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and
// trying to preserve the zoom level and the center of the image
- SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes();
int rotatedOrientation = getRotatedOrientation(orientation);
suggestedCrop = suggestedCrops.get(rotatedOrientation);
- Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ Point suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation);
if (suggestedCrop != null) {
// only keep the visible part (without parallax)
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -197,9 +188,9 @@ public class WallpaperCropper {
// Case 4: if the device is a foldable, if we're looking for a folded orientation and have
// the suggested crop of the relative unfolded orientation, reuse it by removing content.
- int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation);
suggestedCrop = suggestedCrops.get(unfoldedOrientation);
- suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation);
+ suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(unfoldedOrientation);
if (suggestedCrop != null) {
// compute the visible part (without parallax) of the unfolded screen
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -207,8 +198,11 @@ public class WallpaperCropper {
Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE);
// if we removed some width, add it back to add a parallax effect
if (res.width() < adjustedCrop.width()) {
- if (rtl) res.left = Math.min(res.left, adjustedCrop.left);
- else res.right = Math.max(res.right, adjustedCrop.right);
+ if (rtl) {
+ res.left = Math.min(res.left, adjustedCrop.left);
+ } else {
+ res.right = Math.max(res.right, adjustedCrop.right);
+ }
// use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX
res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD);
}
@@ -218,9 +212,9 @@ public class WallpaperCropper {
// Case 5: if the device is a foldable, if we're looking for an unfolded orientation and
// have the suggested crop of the relative folded orientation, reuse it by adding content.
- int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation);
+ int foldedOrientation = defaultDisplayInfo.getFoldedOrientation(orientation);
suggestedCrop = suggestedCrops.get(foldedOrientation);
- suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation);
+ suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(foldedOrientation);
if (suggestedCrop != null) {
// only keep the visible part (without parallax)
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -229,17 +223,19 @@ public class WallpaperCropper {
// Case 6: for a foldable device, try to combine case 3 + case 4 or 5:
// rotate, then fold or unfold
- Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ Point rotatedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation);
if (rotatedDisplaySize != null) {
- int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation);
- int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation);
+ int rotatedFolded = defaultDisplayInfo.getFoldedOrientation(rotatedOrientation);
+ int rotateUnfolded = defaultDisplayInfo.getUnfoldedOrientation(rotatedOrientation);
for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) {
suggestedCrop = suggestedCrops.get(suggestedOrientation);
if (suggestedCrop != null) {
- Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ Rect rotatedCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl);
SparseArray<Rect> rotatedCropMap = new SparseArray<>();
rotatedCropMap.put(rotatedOrientation, rotatedCrop);
- return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, rotatedCropMap,
+ rtl);
}
}
}
@@ -248,8 +244,8 @@ public class WallpaperCropper {
Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize
+ ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops
+ ", orientation: " + orientation + ", rtl: " + rtl
- + ", defaultDisplaySizes: " + defaultDisplaySizes);
- return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ + ", defaultDisplaySizes: " + defaultDisplayInfo.defaultDisplaySizes);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), rtl);
}
/**
@@ -445,7 +441,7 @@ public class WallpaperCropper {
Rect suggestedCrop = suggestedCrops.get(orientation);
if (suggestedCrop != null) {
adjustedSuggestedCrops.put(orientation,
- getCrop(displaySize, bitmapSize, suggestedCrops, rtl));
+ getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, suggestedCrops, rtl));
}
}
@@ -455,7 +451,8 @@ public class WallpaperCropper {
int orientation = defaultDisplaySizes.keyAt(i);
if (result.contains(orientation)) continue;
Point displaySize = defaultDisplaySizes.valueAt(i);
- Rect newCrop = getCrop(displaySize, bitmapSize, adjustedSuggestedCrops, rtl);
+ Rect newCrop = getCrop(displaySize, mDefaultDisplayInfo, bitmapSize,
+ adjustedSuggestedCrops, rtl);
result.put(orientation, newCrop);
}
return result;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index ba0262a8bd19..69f0ef7c430e 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -542,9 +542,11 @@ public class WallpaperDataParser {
// to support back compatibility in B&R, save the crops for one orientation in the
// legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries
int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet;
- if (mWallpaperDisplayHelper.isFoldable()) {
- int unfoldedOrientation = mWallpaperDisplayHelper
- .getUnfoldedOrientation(orientationToPutInLegacyCrop);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
+ if (defaultDisplayInfo.isFoldable) {
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(
+ orientationToPutInLegacyCrop);
if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
orientationToPutInLegacyCrop = unfoldedOrientation;
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java
new file mode 100644
index 000000000000..dabe91968338
--- /dev/null
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+import static android.app.WallpaperManager.getRotatedOrientation;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+
+import android.app.WallpaperManager;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+
+/** A data class for the default display attributes used in wallpaper related operations. */
+public final class WallpaperDefaultDisplayInfo {
+ /**
+ * A data class representing the screen orientations for a foldable device in the folded and
+ * unfolded states.
+ */
+ @VisibleForTesting
+ static final class FoldableOrientations {
+ @WallpaperManager.ScreenOrientation
+ public final int foldedOrientation;
+ @WallpaperManager.ScreenOrientation
+ public final int unfoldedOrientation;
+
+ FoldableOrientations(int foldedOrientation, int unfoldedOrientation) {
+ this.foldedOrientation = foldedOrientation;
+ this.unfoldedOrientation = unfoldedOrientation;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof FoldableOrientations that)) return false;
+ return foldedOrientation == that.foldedOrientation
+ && unfoldedOrientation == that.unfoldedOrientation;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(foldedOrientation, unfoldedOrientation);
+ }
+ }
+
+ public final SparseArray<Point> defaultDisplaySizes;
+ public final boolean isLargeScreen;
+ public final boolean isFoldable;
+ @VisibleForTesting
+ final List<FoldableOrientations> foldableOrientations;
+
+ public WallpaperDefaultDisplayInfo() {
+ this.defaultDisplaySizes = new SparseArray<>();
+ this.isLargeScreen = false;
+ this.isFoldable = false;
+ this.foldableOrientations = Collections.emptyList();
+ }
+
+ public WallpaperDefaultDisplayInfo(WindowManager windowManager, Resources resources) {
+ Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
+ boolean isFoldable = resources.getIntArray(R.array.config_foldedDeviceStates).length > 0;
+ if (isFoldable) {
+ this.foldableOrientations = getFoldableOrientations(metrics);
+ } else {
+ this.foldableOrientations = Collections.emptyList();
+ }
+ this.defaultDisplaySizes = getDisplaySizes(metrics);
+ this.isLargeScreen = isLargeScreen(metrics);
+ this.isFoldable = isFoldable;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof WallpaperDefaultDisplayInfo that)) return false;
+ return isLargeScreen == that.isLargeScreen && isFoldable == that.isFoldable
+ && defaultDisplaySizes.contentEquals(that.defaultDisplaySizes)
+ && Objects.equals(foldableOrientations, that.foldableOrientations);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Objects.hash(isLargeScreen, isFoldable, foldableOrientations)
+ + defaultDisplaySizes.contentHashCode();
+ }
+
+ /**
+ * Returns the folded orientation corresponds to the {@code unfoldedOrientation} found in
+ * {@link #foldableOrientations}. If not found, returns
+ * {@link WallpaperManager.ORIENTATION_UNKNOWN}.
+ */
+ public int getFoldedOrientation(int unfoldedOrientation) {
+ for (FoldableOrientations orientations : foldableOrientations) {
+ if (orientations.unfoldedOrientation == unfoldedOrientation) {
+ return orientations.foldedOrientation;
+ }
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ /**
+ * Returns the unfolded orientation corresponds to the {@code foldedOrientation} found in
+ * {@link #foldableOrientations}. If not found, returns
+ * {@link WallpaperManager.ORIENTATION_UNKNOWN}.
+ */
+ public int getUnfoldedOrientation(int foldedOrientation) {
+ for (FoldableOrientations orientations : foldableOrientations) {
+ if (orientations.foldedOrientation == foldedOrientation) {
+ return orientations.unfoldedOrientation;
+ }
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ private static SparseArray<Point> getDisplaySizes(Set<WindowMetrics> displayMetrics) {
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ for (WindowMetrics metric : displayMetrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+ Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
+ for (Point point : List.of(displaySize, reversedDisplaySize)) {
+ int orientation = WallpaperManager.getOrientation(point);
+ // don't add an entry if there is already a larger display of the same orientation
+ Point display = displaySizes.get(orientation);
+ if (display == null || display.x * display.y < point.x * point.y) {
+ displaySizes.put(orientation, point);
+ }
+ }
+ }
+ return displaySizes;
+ }
+
+ private static boolean isLargeScreen(Set<WindowMetrics> displayMetrics) {
+ float smallestWidth = Float.MAX_VALUE;
+ for (WindowMetrics metric : displayMetrics) {
+ Rect bounds = metric.getBounds();
+ smallestWidth = Math.min(smallestWidth, bounds.width() / metric.getDensity());
+ }
+ return smallestWidth >= LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+ }
+
+ /**
+ * Determines all potential foldable orientations, populating {@code
+ * outFoldableOrientationPairs} with pairs of (folded orientation, unfolded orientation). If
+ * {@code defaultDisplayMetrics} isn't for foldable, {@code outFoldableOrientationPairs} will
+ * not be populated.
+ */
+ private static List<FoldableOrientations> getFoldableOrientations(
+ Set<WindowMetrics> defaultDisplayMetrics) {
+ if (defaultDisplayMetrics.size() != 2) {
+ return Collections.emptyList();
+ }
+ List<FoldableOrientations> foldableOrientations = new ArrayList<>();
+ float surface = 0;
+ int firstOrientation = -1;
+ for (WindowMetrics metric : defaultDisplayMetrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+
+ int orientation = WallpaperManager.getOrientation(displaySize);
+ float newSurface = displaySize.x * displaySize.y
+ / (metric.getDensity() * metric.getDensity());
+ if (surface <= 0) {
+ surface = newSurface;
+ firstOrientation = orientation;
+ } else {
+ FoldableOrientations orientations = (newSurface > surface)
+ ? new FoldableOrientations(firstOrientation, orientation)
+ : new FoldableOrientations(orientation, firstOrientation);
+ FoldableOrientations rotatedOrientations = new FoldableOrientations(
+ getRotatedOrientation(orientations.foldedOrientation),
+ getRotatedOrientation(orientations.unfoldedOrientation));
+ foldableOrientations.add(orientations);
+ foldableOrientations.add(rotatedOrientations);
+ }
+ }
+ return Collections.unmodifiableList(foldableOrientations);
+ }
+}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index 3636f5aa8f27..bff5fc9c49f3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -16,31 +16,25 @@
package com.android.server.wallpaper;
-import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
-import static android.app.WallpaperManager.getRotatedOrientation;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.window.flags.Flags.multiCrop;
import android.app.WallpaperManager;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.Debug;
-import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.WindowManager;
-import android.view.WindowMetrics;
import com.android.server.wm.WindowManagerInternal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
import java.util.function.Consumer;
/**
@@ -59,65 +53,25 @@ class WallpaperDisplayHelper {
}
private static final String TAG = WallpaperDisplayHelper.class.getSimpleName();
- private static final float LARGE_SCREEN_MIN_DP = 600f;
private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>();
private final DisplayManager mDisplayManager;
private final WindowManagerInternal mWindowManagerInternal;
- private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>();
- // related orientations pairs for foldable (folded orientation, unfolded orientation)
- private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>();
-
- private final boolean mIsFoldable;
- private boolean mIsLargeScreen = false;
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
WallpaperDisplayHelper(
DisplayManager displayManager,
WindowManager windowManager,
WindowManagerInternal windowManagerInternal,
- boolean isFoldable) {
+ Resources resources) {
mDisplayManager = displayManager;
mWindowManagerInternal = windowManagerInternal;
- mIsFoldable = isFoldable;
- if (!multiCrop()) return;
- Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
- boolean populateOrientationPairs = isFoldable && metrics.size() == 2;
- float surface = 0;
- int firstOrientation = -1;
- for (WindowMetrics metric: metrics) {
- Rect bounds = metric.getBounds();
- Point displaySize = new Point(bounds.width(), bounds.height());
- Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
- for (Point point : List.of(displaySize, reversedDisplaySize)) {
- int orientation = WallpaperManager.getOrientation(point);
- // don't add an entry if there is already a larger display of the same orientation
- Point display = mDefaultDisplaySizes.get(orientation);
- if (display == null || display.x * display.y < point.x * point.y) {
- mDefaultDisplaySizes.put(orientation, point);
- }
- }
-
- mIsLargeScreen |= (displaySize.x / metric.getDensity() >= LARGE_SCREEN_MIN_DP);
-
- if (populateOrientationPairs) {
- int orientation = WallpaperManager.getOrientation(displaySize);
- float newSurface = displaySize.x * displaySize.y
- / (metric.getDensity() * metric.getDensity());
- if (surface <= 0) {
- surface = newSurface;
- firstOrientation = orientation;
- } else {
- Pair<Integer, Integer> pair = (newSurface > surface)
- ? new Pair<>(firstOrientation, orientation)
- : new Pair<>(orientation, firstOrientation);
- Pair<Integer, Integer> rotatedPair = new Pair<>(
- getRotatedOrientation(pair.first), getRotatedOrientation(pair.second));
- mFoldableOrientationPairs.add(pair);
- mFoldableOrientationPairs.add(rotatedPair);
- }
- }
+ if (!multiCrop()) {
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo();
+ return;
}
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources);
}
DisplayData getDisplayDataOrCreate(int displayId) {
@@ -203,51 +157,21 @@ class WallpaperDisplayHelper {
}
SparseArray<Point> getDefaultDisplaySizes() {
- return mDefaultDisplaySizes;
+ return mDefaultDisplayInfo.defaultDisplaySizes;
}
/** Return the number of pixel of the largest dimension of the default display */
int getDefaultDisplayLargestDimension() {
+ SparseArray<Point> defaultDisplaySizes = mDefaultDisplayInfo.defaultDisplaySizes;
int result = -1;
- for (int i = 0; i < mDefaultDisplaySizes.size(); i++) {
- Point size = mDefaultDisplaySizes.valueAt(i);
+ for (int i = 0; i < defaultDisplaySizes.size(); i++) {
+ Point size = defaultDisplaySizes.valueAt(i);
result = Math.max(result, Math.max(size.x, size.y));
}
return result;
}
- boolean isFoldable() {
- return mIsFoldable;
- }
-
- /**
- * Return true if any of the screens of the default display is considered large (DP >= 600)
- */
- boolean isLargeScreen() {
- return mIsLargeScreen;
- }
-
- /**
- * If a given orientation corresponds to an unfolded orientation on foldable, return the
- * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
- * device is not a foldable.
- */
- int getFoldedOrientation(int orientation) {
- for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
- if (pair.second.equals(orientation)) return pair.first;
- }
- return ORIENTATION_UNKNOWN;
- }
-
- /**
- * If a given orientation corresponds to a folded orientation on foldable, return the
- * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
- * device is not a foldable.
- */
- int getUnfoldedOrientation(int orientation) {
- for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
- if (pair.first.equals(orientation)) return pair.second;
- }
- return ORIENTATION_UNKNOWN;
+ public WallpaperDefaultDisplayInfo getDefaultDisplayInfo() {
+ return mDefaultDisplayInfo;
}
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index bac732637d8d..e7da33d50b27 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1666,12 +1666,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
displayManager.registerDisplayListener(mDisplayListener, null /* handler */);
WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- boolean isFoldable = mContext.getResources()
- .getIntArray(R.array.config_foldedDeviceStates).length > 0;
mWallpaperDisplayHelper = new WallpaperDisplayHelper(
- displayManager, windowManager, mWindowManagerInternal, isFoldable);
+ displayManager, windowManager, mWindowManagerInternal, mContext.getResources());
mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper);
- mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop);
mActivityManager = mContext.getSystemService(ActivityManager.class);
if (mContext.getResources().getBoolean(
@@ -2510,9 +2507,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
List<Rect> result = new ArrayList<>();
boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
for (Point displaySize : displaySizes) {
- result.add(mWallpaperCropper.getCrop(
- displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
+ result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo,
+ croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
}
if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result);
return result;
@@ -2548,8 +2547,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
List<Rect> result = new ArrayList<>();
boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
for (Point displaySize : displaySizes) {
- result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl));
+ result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, bitmapSize,
+ defaultCrops, rtl));
}
return result;
}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index a94183849bc5..e2b47b92f232 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -112,7 +112,6 @@ import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.apphibernation.AppHibernationManagerInternal;
import com.android.server.apphibernation.AppHibernationService;
-import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -807,14 +806,8 @@ class ActivityMetricsLogger {
}
final Task otherTask = otherInfo.mLastLaunchedActivity.getTask();
// The adjacent task is the split root in which activities are started
- final boolean isDescendantOfAdjacent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks(
- otherTask::isDescendantOf);
- } else {
- isDescendantOfAdjacent = otherTask.isDescendantOf(
- launchedSplitRootTask.getAdjacentTask());
- }
+ final boolean isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks(
+ otherTask::isDescendantOf);
if (isDescendantOfAdjacent) {
if (DEBUG_METRICS) {
Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3cd4db7d8dfc..e91d88901751 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1716,6 +1716,7 @@ final class ActivityRecord extends WindowToken {
}
mAppCompatController.getLetterboxPolicy().onMovedToDisplay(mDisplayContent.getDisplayId());
+ mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay();
}
void layoutLetterboxIfNeeded(WindowState winHint) {
@@ -3801,19 +3802,10 @@ final class ActivityRecord extends WindowToken {
final TaskFragment taskFragment = getTaskFragment();
if (next != null && taskFragment != null && taskFragment.isEmbedded()) {
final TaskFragment organized = taskFragment.getOrganizedTaskFragment();
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- delayRemoval = organized != null
- && organized.topRunningActivity() == null
- && organized.isDelayLastActivityRemoval()
- && organized.forOtherAdjacentTaskFragments(next::isDescendantOf);
- } else {
- final TaskFragment adjacent =
- organized != null ? organized.getAdjacentTaskFragment() : null;
- if (adjacent != null && next.isDescendantOf(adjacent)
- && organized.topRunningActivity() == null) {
- delayRemoval = organized.isDelayLastActivityRemoval();
- }
- }
+ delayRemoval = organized != null
+ && organized.topRunningActivity() == null
+ && organized.isDelayLastActivityRemoval()
+ && organized.forOtherAdjacentTaskFragments(next::isDescendantOf);
}
// isNextNotYetVisible is to check if the next activity is invisible, or it has been
@@ -4787,11 +4779,6 @@ final class ActivityRecord extends WindowToken {
}
// Make sure the embedded adjacent can also be shown.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final ActivityRecord adjacentActivity = taskFragment.getAdjacentTaskFragment()
- .getTopNonFinishingActivity();
- return canShowWhenLocked(adjacentActivity);
- }
final boolean hasAdjacentNotAllowToShow = taskFragment.forOtherAdjacentTaskFragments(
adjacentTF -> !canShowWhenLocked(adjacentTF.getTopNonFinishingActivity()));
return !hasAdjacentNotAllowToShow;
@@ -8980,6 +8967,7 @@ final class ActivityRecord extends WindowToken {
// Reset the existing override configuration so it can be updated according to the latest
// configuration.
mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode();
+ mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted();
if (!attachedToProcess()) {
return;
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index cb122f2080a2..0f1939bfbb49 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -34,7 +34,6 @@ import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
-import com.android.window.flags.Flags;
import java.io.File;
import java.io.PrintWriter;
@@ -532,26 +531,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord
final int currentIndex = currTF.asTask() != null
? currentTask.mChildren.indexOf(currentActivity)
: currentTask.mChildren.indexOf(currTF);
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final int prevAdjacentIndex = currentTask.mChildren.indexOf(
- prevTF.getAdjacentTaskFragment());
- if (prevAdjacentIndex > currentIndex) {
- // PrevAdjacentTF already above currentActivity
- return;
- }
- // Add both the one below, and its adjacent.
- if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
- result.add(initPrev);
- }
- final ActivityRecord prevAdjacentActivity = prevTF.getAdjacentTaskFragment()
- .getTopMostActivity();
- if (prevAdjacentActivity != null && (!inTransition
- || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) {
- result.add(prevAdjacentActivity);
- }
- return;
- }
-
final boolean hasAdjacentAboveCurrent = prevTF.forOtherAdjacentTaskFragments(
prevAdjacentTF -> {
final int prevAdjacentIndex = currentTask.mChildren.indexOf(prevAdjacentTF);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index a7f2153993bb..b0563128870a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -164,7 +164,6 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.pm.SaferIntentUtils;
import com.android.server.utils.Slogf;
import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
-import com.android.window.flags.Flags;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -2991,17 +2990,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
if (child.asTaskFragment() != null
&& child.asTaskFragment().hasAdjacentTaskFragment()) {
- final boolean isAnyTranslucent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment.AdjacentSet set =
- child.asTaskFragment().getAdjacentTaskFragments();
- isAnyTranslucent = set.forAllTaskFragments(
- tf -> !isOpaque(tf), null);
- } else {
- final TaskFragment adjacent = child.asTaskFragment()
- .getAdjacentTaskFragment();
- isAnyTranslucent = !isOpaque(child) || !isOpaque(adjacent);
- }
+ final boolean isAnyTranslucent = !isOpaque(child)
+ || child.asTaskFragment().forOtherAdjacentTaskFragments(
+ tf -> !isOpaque(tf));
if (!isAnyTranslucent) {
// This task fragment and all its adjacent task fragments are opaque,
// consider it opaque even if it doesn't fill its parent.
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 48f08e945a59..c479591a5e0d 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -46,6 +46,8 @@ class AppCompatController {
private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy;
@NonNull
private final AppCompatSandboxingPolicy mSandboxingPolicy;
+ @NonNull
+ private final AppCompatDisplayCompatModePolicy mDisplayCompatModePolicy;
AppCompatController(@NonNull WindowManagerService wmService,
@NonNull ActivityRecord activityRecord) {
@@ -69,6 +71,7 @@ class AppCompatController {
mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord,
mAppCompatOverrides);
mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord);
+ mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy();
}
@NonNull
@@ -151,6 +154,11 @@ class AppCompatController {
return mSandboxingPolicy;
}
+ @NonNull
+ AppCompatDisplayCompatModePolicy getDisplayCompatModePolicy() {
+ return mDisplayCompatModePolicy;
+ }
+
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
getTransparentPolicy().dump(pw, prefix);
getLetterboxPolicy().dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java
new file mode 100644
index 000000000000..acf51707c894
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import com.android.window.flags.Flags;
+
+/**
+ * Encapsulate app-compat logic for multi-display environments.
+ */
+class AppCompatDisplayCompatModePolicy {
+
+ private boolean mIsRestartMenuEnabledForDisplayMove;
+
+ boolean isRestartMenuEnabledForDisplayMove() {
+ return Flags.enableRestartMenuForConnectedDisplays() && mIsRestartMenuEnabledForDisplayMove;
+ }
+
+ void onMovedToDisplay() {
+ mIsRestartMenuEnabledForDisplayMove = true;
+ }
+
+ void onProcessRestarted() {
+ mIsRestartMenuEnabledForDisplayMove = false;
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index b03aa5263927..0f1e36d70db2 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -361,7 +361,10 @@ class AppCompatSizeCompatModePolicy {
if (enableSizeCompatModeImprovementsForConnectedDisplays()) {
overrideConfig.touchscreen = fullConfig.touchscreen;
overrideConfig.navigation = fullConfig.navigation;
- overrideConfig.fontScale = fullConfig.fontScale;
+ overrideConfig.keyboard = fullConfig.keyboard;
+ overrideConfig.keyboardHidden = fullConfig.keyboardHidden;
+ overrideConfig.hardKeyboardHidden = fullConfig.hardKeyboardHidden;
+ overrideConfig.navigationHidden = fullConfig.navigationHidden;
}
// The smallest screen width is the short side of screen bounds. Because the bounds
// and density won't be changed, smallestScreenWidthDp is also fixed.
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 146044008b3f..b91a12598e01 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -161,6 +161,9 @@ final class AppCompatUtils {
top.mAppCompatController.getLetterboxOverrides()
.isLetterboxEducationEnabled());
+ appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(top.mAppCompatController
+ .getDisplayCompatModePolicy().isRestartMenuEnabledForDisplayMove());
+
final AppCompatAspectRatioOverrides aspectRatioOverrides =
top.mAppCompatController.getAspectRatioOverrides();
appCompatTaskInfo.setUserFullscreenOverrideEnabled(
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e9b7649e8cbd..dfe323c43abb 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -479,21 +479,16 @@ class BackNavigationController {
}
} else {
// If adjacent TF has companion to current TF, those two TF will be closed together.
- final TaskFragment adjacentTF;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (currTF.getAdjacentTaskFragments().size() > 2) {
- throw new IllegalStateException(
- "Not yet support 3+ adjacent for non-Task TFs");
- }
- final TaskFragment[] tmpAdjacent = new TaskFragment[1];
- currTF.forOtherAdjacentTaskFragments(tf -> {
- tmpAdjacent[0] = tf;
- return true;
- });
- adjacentTF = tmpAdjacent[0];
- } else {
- adjacentTF = currTF.getAdjacentTaskFragment();
+ if (currTF.getAdjacentTaskFragments().size() > 2) {
+ throw new IllegalStateException(
+ "Not yet support 3+ adjacent for non-Task TFs");
}
+ final TaskFragment[] tmpAdjacent = new TaskFragment[1];
+ currTF.forOtherAdjacentTaskFragments(tf -> {
+ tmpAdjacent[0] = tf;
+ return true;
+ });
+ final TaskFragment adjacentTF = tmpAdjacent[0];
if (isSecondCompanionToFirst(currTF, adjacentTF)) {
// The two TFs are adjacent (visually displayed side-by-side), search if any
// activity below the lowest one.
@@ -553,15 +548,6 @@ class BackNavigationController {
if (!prevTF.hasAdjacentTaskFragment()) {
return;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment();
- final ActivityRecord prevActivityAdjacent =
- prevTFAdjacent.getTopNonFinishingActivity();
- if (prevActivityAdjacent != null) {
- outPrevActivities.add(prevActivityAdjacent);
- }
- return;
- }
prevTF.forOtherAdjacentTaskFragments(prevTFAdjacent -> {
final ActivityRecord prevActivityAdjacent =
prevTFAdjacent.getTopNonFinishingActivity();
@@ -577,14 +563,6 @@ class BackNavigationController {
if (mainTF == null || !mainTF.hasAdjacentTaskFragment()) {
return;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment();
- final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity();
- if (topActivity != null) {
- outList.add(topActivity);
- }
- return;
- }
mainTF.forOtherAdjacentTaskFragments(adjacentTF -> {
final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity();
if (topActivity != null) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2287a687700c..f50a68cc5389 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -49,8 +49,8 @@ import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskS
import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
import static com.android.window.flags.Flags.balShowToastsBlocked;
-import static com.android.window.flags.Flags.balStrictModeRo;
import static com.android.window.flags.Flags.balStrictModeGracePeriod;
+import static com.android.window.flags.Flags.balStrictModeRo;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static java.util.Objects.requireNonNull;
@@ -91,7 +91,6 @@ import com.android.internal.util.Preconditions;
import com.android.server.UiThread;
import com.android.server.am.PendingIntentRecord;
import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration;
-import com.android.window.flags.Flags;
import java.lang.annotation.Retention;
import java.util.ArrayList;
@@ -1687,14 +1686,6 @@ public class BackgroundActivityStartController {
}
// Check the adjacent fragment.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
- topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate);
- if (topActivity == null) {
- return bas;
- }
- return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas);
- }
final BlockActivityStart[] out = { bas };
taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> {
final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate);
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index f473b7b7e4fb..fcc697242ff6 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -18,7 +18,7 @@ package com.android.server.wm;
import static android.view.WindowManager.TRANSIT_CHANGE;
-import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS;
+import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
@@ -140,8 +140,9 @@ class DeferredDisplayUpdater {
if (displayInfoDiff == DIFF_EVERYTHING
|| !mDisplayContent.getLastHasContent()
|| !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: applying DisplayInfo immediately");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) immediately",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
mLastWmDisplayInfo = displayInfo;
applyLatestDisplayInfo();
@@ -151,17 +152,23 @@ class DeferredDisplayUpdater {
// If there are non WM-specific display info changes, apply only these fields immediately
if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: partially applying DisplayInfo immediately");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: partially applying DisplayInfo(%d x %d) immediately",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
applyLatestDisplayInfo();
}
// If there are WM-specific display info changes, apply them through a Shell transition
if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: deferring DisplayInfo update");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: deferring DisplayInfo(%d x %d) update",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
requestDisplayChangeTransition(physicalDisplayUpdated, () -> {
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) after deferring",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
+
// Apply deferrable fields to DisplayContent only when the transition
// starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo
mLastWmDisplayInfo = displayInfo;
@@ -199,7 +206,7 @@ class DeferredDisplayUpdater {
mDisplayContent.getDisplayPolicy().getNotificationShade();
if (notificationShade != null && notificationShade.isVisible()
&& mDisplayContent.mAtmService.mKeyguardController.isKeyguardOrAodShowing(
- mDisplayContent.mDisplayId)) {
+ mDisplayContent.mDisplayId)) {
Slog.i(TAG, notificationShade + " uses blast for display switch");
notificationShade.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST;
}
@@ -209,9 +216,6 @@ class DeferredDisplayUpdater {
try {
onStartCollect.run();
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: applied DisplayInfo after deferring");
-
if (physicalDisplayUpdated) {
onDisplayUpdated(transition, fromRotation, startBounds);
} else {
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index dc42b32967e2..d91fca9e2816 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -17,6 +17,7 @@
package com.android.server.wm;
import static android.app.Flags.enableConnectedDisplaysWallpaper;
+import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE;
import android.annotation.NonNull;
import android.content.Context;
@@ -66,7 +67,7 @@ public final class DesktopModeHelper {
* Return {@code true} if the current device can hosts desktop sessions on its internal display.
*/
@VisibleForTesting
- static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ private static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
}
@@ -83,8 +84,11 @@ public final class DesktopModeHelper {
if (!shouldEnforceDeviceRestrictions()) {
return true;
}
- final boolean desktopModeSupported = isDesktopModeSupported(context)
- && canInternalDisplayHostDesktops(context);
+ // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a
+ // requirement.
+ final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
+ ? isDesktopModeSupported(context) : (isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context));
final boolean desktopModeSupportedByDevOptions =
Flags.enableDesktopModeThroughDevOption()
&& isDesktopModeDevOptionsSupported(context);
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index d466a646c7dd..ddcb5eccb1d8 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -19,6 +19,12 @@ package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -131,6 +137,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
return RESULT_SKIP;
}
+ if (DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue()) {
+ ActivityRecord topVisibleFreeformActivity =
+ task.getDisplayContent().getTopMostVisibleFreeformActivity();
+ if (shouldInheritExistingTaskBounds(topVisibleFreeformActivity, activity, task)) {
+ appendLog("inheriting bounds from existing closing instance");
+ outParams.mBounds.set(topVisibleFreeformActivity.getBounds());
+ appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
+ // Return result done to prevent other modifiers from changing or cascading bounds.
+ return RESULT_DONE;
+ }
+ }
+
DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options,
outParams.mBounds, this::appendLog);
appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
@@ -159,7 +177,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
// activity will also enter desktop mode. On this same relationship, we can also assume
// if there are not visible freeform tasks but a freeform activity is now launching, it
// will force the device into desktop mode.
- return (task.getDisplayContent().getTopMostVisibleFreeformActivity() != null
+ return (task.getDisplayContent().getTopMostFreeformActivity() != null
&& checkSourceWindowModesCompatible(task, options, currentParams))
|| isRequestingFreeformWindowMode(task, options, currentParams);
}
@@ -201,6 +219,40 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
};
}
+ /**
+ * Whether the launching task should inherit the task bounds of an existing closing instance.
+ */
+ private boolean shouldInheritExistingTaskBounds(
+ @Nullable ActivityRecord existingTaskActivity,
+ @Nullable ActivityRecord launchingActivity,
+ @NonNull Task launchingTask) {
+ if (existingTaskActivity == null || launchingActivity == null) return false;
+ return (existingTaskActivity.packageName == launchingActivity.packageName)
+ && isLaunchingNewTask(launchingActivity.launchMode,
+ launchingTask.getBaseIntent().getFlags())
+ && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags());
+ }
+
+ /**
+ * Returns true if the launch mode or intent will result in a new task being created for the
+ * activity.
+ */
+ private boolean isLaunchingNewTask(int launchMode, int intentFlags) {
+ return launchMode == LAUNCH_SINGLE_TASK
+ || launchMode == LAUNCH_SINGLE_INSTANCE
+ || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK
+ || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0;
+ }
+
+ /**
+ * Returns true if the intent will result in an existing task instance being closed if a new
+ * one appears.
+ */
+ private boolean isClosingExitingInstance(int intentFlags) {
+ return (intentFlags & FLAG_ACTIVITY_CLEAR_TASK) != 0
+ || (intentFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0;
+ }
+
private void initLogBuilder(Task task, ActivityRecord activity) {
if (DEBUG) {
mLogBuilder = new StringBuilder(
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index a017a1173d97..e508a6d23178 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -23,8 +23,6 @@ import static com.android.server.wm.Task.TAG_VISIBILITY;
import android.annotation.Nullable;
import android.util.Slog;
-import com.android.window.flags.Flags;
-
import java.util.ArrayList;
/** Helper class to ensure activities are in the right visible state for a container. */
@@ -112,18 +110,11 @@ class EnsureActivitiesVisibleHelper {
if (adjacentTaskFragments != null && adjacentTaskFragments.contains(
childTaskFragment)) {
- final boolean isTranslucent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- isTranslucent = childTaskFragment.isTranslucent(starting)
- || childTaskFragment.forOtherAdjacentTaskFragments(
- adjacentTaskFragment -> {
- return adjacentTaskFragment.isTranslucent(starting);
- });
- } else {
- isTranslucent = childTaskFragment.isTranslucent(starting)
- || childTaskFragment.getAdjacentTaskFragment()
- .isTranslucent(starting);
- }
+ final boolean isTranslucent = childTaskFragment.isTranslucent(starting)
+ || childTaskFragment.forOtherAdjacentTaskFragments(
+ adjacentTaskFragment -> {
+ return adjacentTaskFragment.isTranslucent(starting);
+ });
if (!isTranslucent) {
// Everything behind two adjacent TaskFragments are occluded.
mBehindFullyOccludedContainer = true;
@@ -135,14 +126,10 @@ class EnsureActivitiesVisibleHelper {
if (adjacentTaskFragments == null) {
adjacentTaskFragments = new ArrayList<>();
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments;
- childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- adjacentTfs.add(adjacentTf);
- });
- } else {
- adjacentTaskFragments.add(childTaskFragment.getAdjacentTaskFragment());
- }
+ final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments;
+ childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ adjacentTfs.add(adjacentTf);
+ });
}
} else if (child.asActivityRecord() != null) {
setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity);
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index cf201c9f34f0..609302ce3f56 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1732,26 +1732,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
activityAssistInfos.clear();
activityAssistInfos.add(new ActivityAssistInfo(top));
// Check if the activity on the split screen.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- top.getTask().forOtherAdjacentTasks(task -> {
- final ActivityRecord adjacentActivityRecord =
- task.getTopNonFinishingActivity();
- if (adjacentActivityRecord != null) {
- activityAssistInfos.add(
- new ActivityAssistInfo(adjacentActivityRecord));
- }
- });
- } else {
- final Task adjacentTask = top.getTask().getAdjacentTask();
- if (adjacentTask != null) {
- final ActivityRecord adjacentActivityRecord =
- adjacentTask.getTopNonFinishingActivity();
- if (adjacentActivityRecord != null) {
- activityAssistInfos.add(
- new ActivityAssistInfo(adjacentActivityRecord));
- }
+ top.getTask().forOtherAdjacentTasks(task -> {
+ final ActivityRecord adjacentActivityRecord =
+ task.getTopNonFinishingActivity();
+ if (adjacentActivityRecord != null) {
+ activityAssistInfos.add(
+ new ActivityAssistInfo(adjacentActivityRecord));
}
- }
+ });
if (rootTask == topFocusedRootTask) {
topVisibleActivities.addAll(0, activityAssistInfos);
} else {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d16c301cec40..e98b2b749af8 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2461,21 +2461,6 @@ class Task extends TaskFragment {
return parentTask == null ? null : parentTask.getCreatedByOrganizerTask();
}
- /** @deprecated b/373709676 replace with {@link #forOtherAdjacentTasks(Consumer)} ()}. */
- @Deprecated
- @Nullable
- Task getAdjacentTask() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. "
- + "Use #forOtherAdjacentTasks instead");
- }
- final Task taskWithAdjacent = getTaskWithAdjacent();
- if (taskWithAdjacent == null) {
- return null;
- }
- return taskWithAdjacent.getAdjacentTaskFragment().asTask();
- }
-
/** Finds the first Task parent (or itself) that has adjacent. */
@Nullable
Task getTaskWithAdjacent() {
@@ -2499,11 +2484,6 @@ class Task extends TaskFragment {
* Tasks. The invoke order is not guaranteed.
*/
void forOtherAdjacentTasks(@NonNull Consumer<Task> callback) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. "
- + "Use #getAdjacentTask instead");
- }
-
final Task taskWithAdjacent = getTaskWithAdjacent();
if (taskWithAdjacent == null) {
return;
@@ -2521,10 +2501,6 @@ class Task extends TaskFragment {
* guaranteed.
*/
boolean forOtherAdjacentTasks(@NonNull Predicate<Task> callback) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. "
- + "Use getAdjacentTask instead");
- }
final Task taskWithAdjacent = getTaskWithAdjacent();
if (taskWithAdjacent == null) {
return false;
@@ -3651,20 +3627,13 @@ class Task extends TaskFragment {
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && taskFragment.isEmbedded()
&& taskFragment.hasAdjacentTaskFragment()) {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final int[] nextLayer = { layer };
- taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, nextLayer[0]++);
- }
- });
- layer = nextLayer[0];
- } else {
- final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
+ final int[] nextLayer = { layer };
+ taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, layer++);
+ adjacentTf.assignLayer(t, nextLayer[0]++);
}
- }
+ });
+ layer = nextLayer[0];
}
// Place the decor surface just above the owner TaskFragment.
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 1966ecf57c73..fb7bab4b3e26 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -60,7 +60,6 @@ import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.LaunchParamsController.LaunchParams;
-import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -1089,19 +1088,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
// Use launch-adjacent-flag-root if launching with launch-adjacent flag.
if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
&& mLaunchAdjacentFlagRootTask != null) {
- final Task launchAdjacentRootAdjacentTask;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final Task[] tmpTask = new Task[1];
- mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> {
- // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+.
- // Find the first adjacent for now.
- tmpTask[0] = task;
- return true;
- });
- launchAdjacentRootAdjacentTask = tmpTask[0];
- } else {
- launchAdjacentRootAdjacentTask = mLaunchAdjacentFlagRootTask.getAdjacentTask();
- }
+ final Task[] tmpTask = new Task[1];
+ mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> {
+ // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+.
+ // Find the first adjacent for now.
+ tmpTask[0] = task;
+ return true;
+ });
+ final Task launchAdjacentRootAdjacentTask = tmpTask[0];
if (sourceTask != null && (sourceTask == candidateTask
|| sourceTask.topRunningActivity() == null)) {
// Do nothing when task that is getting opened is same as the source or when
@@ -1129,14 +1123,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
if (launchRootTask == null || sourceTask == null) {
return launchRootTask;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final Task adjacentRootTask = launchRootTask.getAdjacentTask();
- if (adjacentRootTask != null && (sourceTask == adjacentRootTask
- || sourceTask.isDescendantOf(adjacentRootTask))) {
- return adjacentRootTask;
- }
- return launchRootTask;
- }
final Task[] adjacentRootTask = new Task[1];
launchRootTask.forOtherAdjacentTasks(task -> {
if (sourceTask == task || sourceTask.isDescendantOf(task)) {
@@ -1163,24 +1149,16 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
return sourceTask.getCreatedByOrganizerTask();
}
// Check if the candidate is already positioned in the adjacent Task.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final Task[] adjacentRootTask = new Task[1];
- sourceTask.forOtherAdjacentTasks(task -> {
- if (candidateTask == task || candidateTask.isDescendantOf(task)) {
- adjacentRootTask[0] = task;
- return true;
- }
- return false;
- });
- if (adjacentRootTask[0] != null) {
- return adjacentRootTask[0];
- }
- } else {
- final Task adjacentTarget = taskWithAdjacent.getAdjacentTask();
- if (candidateTask == adjacentTarget
- || candidateTask.isDescendantOf(adjacentTarget)) {
- return adjacentTarget;
+ final Task[] adjacentRootTask = new Task[1];
+ sourceTask.forOtherAdjacentTasks(task -> {
+ if (candidateTask == task || candidateTask.isDescendantOf(task)) {
+ adjacentRootTask[0] = task;
+ return true;
}
+ return false;
+ });
+ if (adjacentRootTask[0] != null) {
+ return adjacentRootTask[0];
}
return sourceTask.getCreatedByOrganizerTask();
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 5183c6b57f15..2dabb253744a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -235,11 +235,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
/** This task fragment will be removed when the cleanup of its children are done. */
private boolean mIsRemovalRequested;
- /** @deprecated b/373709676 replace with {@link #mAdjacentTaskFragments} */
- @Deprecated
- @Nullable
- private TaskFragment mAdjacentTaskFragment;
-
/**
* The TaskFragments that are adjacent to each other, including this TaskFragment.
* All TaskFragments in this set share the same set instance.
@@ -455,22 +450,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return service.mWindowOrganizerController.getTaskFragment(token);
}
- /** @deprecated b/373709676 replace with {@link #setAdjacentTaskFragments}. */
- @Deprecated
- void setAdjacentTaskFragment(@NonNull TaskFragment taskFragment) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- if (mAdjacentTaskFragment == taskFragment) {
- return;
- }
- resetAdjacentTaskFragment();
- mAdjacentTaskFragment = taskFragment;
- taskFragment.setAdjacentTaskFragment(this);
- return;
- }
-
- setAdjacentTaskFragments(new AdjacentSet(this, taskFragment));
- }
-
void setAdjacentTaskFragments(@NonNull AdjacentSet adjacentTaskFragments) {
adjacentTaskFragments.setAsAdjacent();
}
@@ -483,56 +462,18 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return mCompanionTaskFragment;
}
- /** @deprecated b/373709676 replace with {@link #clearAdjacentTaskFragments()}. */
- @Deprecated
- private void resetAdjacentTaskFragment() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("resetAdjacentTaskFragment shouldn't be called when"
- + " allowMultipleAdjacentTaskFragments is enabled. Use either"
- + " #clearAdjacentTaskFragments or #removeFromAdjacentTaskFragments");
- }
- // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment.
- if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) {
- mAdjacentTaskFragment.mAdjacentTaskFragment = null;
- mAdjacentTaskFragment.mDelayLastActivityRemoval = false;
- }
- mAdjacentTaskFragment = null;
- mDelayLastActivityRemoval = false;
- }
-
void clearAdjacentTaskFragments() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- resetAdjacentTaskFragment();
- return;
- }
-
if (mAdjacentTaskFragments != null) {
mAdjacentTaskFragments.clear();
}
}
void removeFromAdjacentTaskFragments() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- resetAdjacentTaskFragment();
- return;
- }
-
if (mAdjacentTaskFragments != null) {
mAdjacentTaskFragments.remove(this);
}
}
- /** @deprecated b/373709676 replace with {@link #getAdjacentTaskFragments()}. */
- @Deprecated
- @Nullable
- TaskFragment getAdjacentTaskFragment() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. "
- + "Use #getAdjacentTaskFragments instead");
- }
- return mAdjacentTaskFragment;
- }
-
@Nullable
AdjacentSet getAdjacentTaskFragments() {
return mAdjacentTaskFragments;
@@ -561,16 +502,10 @@ class TaskFragment extends WindowContainer<WindowContainer> {
}
boolean hasAdjacentTaskFragment() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return mAdjacentTaskFragment != null;
- }
return mAdjacentTaskFragments != null;
}
boolean isAdjacentTo(@NonNull TaskFragment other) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return mAdjacentTaskFragment == other;
- }
return other != this
&& mAdjacentTaskFragments != null
&& mAdjacentTaskFragments.contains(other);
@@ -1377,21 +1312,12 @@ class TaskFragment extends WindowContainer<WindowContainer> {
if (taskFragment.isAdjacentTo(this)) {
continue;
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds())
- || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- return mTmpRect.intersect(adjacentTf.getBounds());
- });
- if (isOccluding) {
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
- } else {
- final TaskFragment adjacentTaskFragment =
- taskFragment.mAdjacentTaskFragment;
- if (mTmpRect.intersect(taskFragment.getBounds())
- || mTmpRect.intersect(adjacentTaskFragment.getBounds())) {
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
+ final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds())
+ || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ return mTmpRect.intersect(adjacentTf.getBounds());
+ });
+ if (isOccluding) {
+ return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
}
}
}
@@ -1427,37 +1353,22 @@ class TaskFragment extends WindowContainer<WindowContainer> {
// 2. Adjacent TaskFragments do not overlap, so that if this TaskFragment is behind
// any translucent TaskFragment in the adjacent set, then this TaskFragment is
// visible behind translucent.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments(
- adjacentTaskFragments::contains);
- if (hasTraversedAdj) {
- final boolean isTranslucent =
- isBehindTransparentTaskFragment(otherTaskFrag, starting)
- || otherTaskFrag.forOtherAdjacentTaskFragments(
- (Predicate<TaskFragment>) tf ->
- isBehindTransparentTaskFragment(tf, starting));
- if (isTranslucent) {
- // Can be visible behind a translucent adjacent TaskFragments.
- gotTranslucentFullscreen = true;
- gotTranslucentAdjacent = true;
- continue;
- }
- // Can not be visible behind adjacent TaskFragments.
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
- } else {
- if (adjacentTaskFragments.contains(otherTaskFrag.mAdjacentTaskFragment)) {
- if (isBehindTransparentTaskFragment(otherTaskFrag, starting)
- || isBehindTransparentTaskFragment(
- otherTaskFrag.mAdjacentTaskFragment, starting)) {
- // Can be visible behind a translucent adjacent TaskFragments.
- gotTranslucentFullscreen = true;
- gotTranslucentAdjacent = true;
- continue;
- }
- // Can not be visible behind adjacent TaskFragments.
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+ final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments(
+ adjacentTaskFragments::contains);
+ if (hasTraversedAdj) {
+ final boolean isTranslucent =
+ isBehindTransparentTaskFragment(otherTaskFrag, starting)
+ || otherTaskFrag.forOtherAdjacentTaskFragments(
+ (Predicate<TaskFragment>) tf ->
+ isBehindTransparentTaskFragment(tf, starting));
+ if (isTranslucent) {
+ // Can be visible behind a translucent adjacent TaskFragments.
+ gotTranslucentFullscreen = true;
+ gotTranslucentAdjacent = true;
+ continue;
}
+ // Can not be visible behind adjacent TaskFragments.
+ return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
}
adjacentTaskFragments.add(otherTaskFrag);
}
@@ -3299,40 +3210,23 @@ class TaskFragment extends WindowContainer<WindowContainer> {
final ArrayList<WindowContainer> siblings = getParent().mChildren;
final int zOrder = siblings.indexOf(this);
-
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- if (siblings.indexOf(getAdjacentTaskFragment()) < zOrder) {
- // early return if this TF already has higher z-ordering.
- return false;
- }
- } else {
- final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments(
- tf -> siblings.indexOf(tf) > zOrder);
- if (!hasAdjacentOnTop) {
- // early return if this TF already has higher z-ordering.
- return false;
- }
+ final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments(
+ tf -> siblings.indexOf(tf) > zOrder);
+ if (!hasAdjacentOnTop) {
+ // early return if this TF already has higher z-ordering.
+ return false;
}
final ToBooleanFunction<WindowState> getDimBehindWindow =
(w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
&& w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
|| w.mActivityRecord.isVisible());
-
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTf = getAdjacentTaskFragment();
- if (adjacentTf.forAllWindows(getDimBehindWindow, true)) {
- // early return if the adjacent Tf has a dimming window.
- return false;
- }
- } else {
- final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> {
- return tf.forAllWindows(getDimBehindWindow, true);
- });
- if (adjacentHasDimmingWindow) {
- // early return if the adjacent Tf has a dimming window.
- return false;
- }
+ final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> {
+ return tf.forAllWindows(getDimBehindWindow, true);
+ });
+ if (adjacentHasDimmingWindow) {
+ // early return if the adjacent Tf has a dimming window.
+ return false;
}
// boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
@@ -3456,16 +3350,9 @@ class TaskFragment extends WindowContainer<WindowContainer> {
sb.append(" organizerProc=");
sb.append(mTaskFragmentOrganizerProcessName);
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (mAdjacentTaskFragments != null) {
- sb.append(" adjacent=");
- sb.append(mAdjacentTaskFragments);
- }
- } else {
- if (mAdjacentTaskFragment != null) {
- sb.append(" adjacent=");
- sb.append(mAdjacentTaskFragment);
- }
+ if (mAdjacentTaskFragments != null) {
+ sb.append(" adjacent=");
+ sb.append(mAdjacentTaskFragments);
}
sb.append('}');
return sb.toString();
@@ -3591,10 +3478,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
}
AdjacentSet(@NonNull ArraySet<TaskFragment> taskFragments) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments must be"
- + " enabled to set more than two TaskFragments adjacent to each other.");
- }
final int size = taskFragments.size();
if (size < 2) {
throw new IllegalArgumentException("Adjacent TaskFragments must contain at least"
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c78cdaa10df2..803c21ccab6e 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2589,9 +2589,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
}
// When the TaskFragment has an adjacent TaskFragment, sibling behind them should be
// hidden unless any of them are translucent.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return taskFragment.getAdjacentTaskFragment().isTranslucentForTransition();
- }
return taskFragment.forOtherAdjacentTaskFragments(TaskFragment::isTranslucentForTransition);
}
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3a4d9d27f65a..e1553cd37d03 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -57,7 +57,8 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
+import com.android.server.wallpaper.WallpaperCropper;
+import com.android.server.wallpaper.WallpaperDefaultDisplayInfo;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -71,7 +72,6 @@ import java.util.function.Consumer;
class WallpaperController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM;
private WindowManagerService mService;
- private WallpaperCropUtils mWallpaperCropUtils = null;
private DisplayContent mDisplayContent;
// Larger index has higher z-order.
@@ -116,6 +116,10 @@ class WallpaperController {
private boolean mShouldOffsetWallpaperCenter;
+ // This is for WallpaperCropper, which has cropping logic for the default display only.
+ // TODO(b/400685784) make the WallpaperCropper operate on every display independently
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
+
private final ToBooleanFunction<WindowState> mFindWallpaperTargetFunction = w -> {
final ActivityRecord ar = w.mActivityRecord;
// The animating window can still be visible on screen if it is in transition, so we
@@ -198,12 +202,14 @@ class WallpaperController {
WallpaperController(WindowManagerService service, DisplayContent displayContent) {
mService = service;
mDisplayContent = displayContent;
+ WindowManager windowManager = service.mContext.getSystemService(WindowManager.class);
Resources resources = service.mContext.getResources();
mMinWallpaperScale =
resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale);
mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale);
mShouldOffsetWallpaperCenter = resources.getBoolean(
com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay);
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources);
}
void resetLargestDisplay(Display display) {
@@ -246,10 +252,6 @@ class WallpaperController {
return largestDisplaySize;
}
- void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
- mWallpaperCropUtils = wallpaperCropUtils;
- }
-
WindowState getWallpaperTarget() {
return mWallpaperTarget;
}
@@ -352,16 +354,12 @@ class WallpaperController {
int offsetY;
if (multiCrop()) {
- if (mWallpaperCropUtils == null) {
- Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting");
- return false;
- }
Point bitmapSize = new Point(
wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight);
SparseArray<Rect> cropHints = token.getCropHints();
wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame()
- : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints,
- wallpaperWin.isRtl());
+ : WallpaperCropper.getCrop(screenSize, mDefaultDisplayInfo, bitmapSize,
+ cropHints, wallpaperWin.isRtl());
int frameWidth = wallpaperFrame.width();
int frameHeight = wallpaperFrame.height();
float frameRatio = (float) frameWidth / frameHeight;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 466ed7863c84..772a7fdfc684 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2006,11 +2006,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return getActivity(r -> !r.finishing, true /* traverseTopToBottom */);
}
- ActivityRecord getTopMostVisibleFreeformActivity() {
+ ActivityRecord getTopMostFreeformActivity() {
return getActivity(r -> r.isVisibleRequested() && r.inFreeformWindowingMode(),
true /* traverseTopToBottom */);
}
+ ActivityRecord getTopMostVisibleFreeformActivity() {
+ return getActivity(r -> r.isVisible() && r.inFreeformWindowingMode(),
+ true /* traverseTopToBottom */);
+ }
+
ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) {
// Break down into 4 calls to avoid object creation due to capturing input params.
if (includeFinishing) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 4b5a3a031931..5f2a2ad7f0eb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -54,7 +54,6 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
import com.android.server.policy.WindowManagerPolicy;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.SensitiveContentPackages.PackageInfo;
import java.lang.annotation.Retention;
@@ -772,12 +771,6 @@ public abstract class WindowManagerInternal {
public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints);
/**
- * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}.
- * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper.
- */
- public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils);
-
- /**
* Returns {@code true} if a Window owned by {@code uid} has focus.
*/
public abstract boolean isUidFocused(int uid);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9fc0339c52a2..c078d67b6cc6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -356,7 +356,6 @@ import com.android.server.policy.WindowManagerPolicy;
import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
import com.android.server.power.ShutdownThread;
import com.android.server.utils.PriorityDump;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.window.flags.Flags;
import dalvik.annotation.optimization.NeverCompile;
@@ -8100,12 +8099,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
- mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController
- .setWallpaperCropUtils(wallpaperCropUtils);
- }
-
- @Override
public boolean isUidFocused(int uid) {
synchronized (mGlobalLock) {
for (int i = mRoot.getChildCount() - 1; i >= 0; i--) {
@@ -9374,23 +9367,6 @@ public class WindowManagerService extends IWindowManager.Stub
return focusedActivity;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
- final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity();
- if (adjacentTopActivity == null) {
- // Return if no adjacent activity.
- return focusedActivity;
- }
-
- if (adjacentTopActivity.getLastWindowCreateTime()
- < focusedActivity.getLastWindowCreateTime()) {
- // Return if the current focus activity has more recently active window.
- return focusedActivity;
- }
-
- return adjacentTopActivity;
- }
-
// Find the adjacent activity with more recently active window.
final ActivityRecord[] mostRecentActiveActivity = { focusedActivity };
final long[] mostRecentActiveTime = { focusedActivity.getLastWindowCreateTime() };
@@ -9461,20 +9437,15 @@ public class WindowManagerService extends IWindowManager.Stub
// No adjacent window.
return false;
}
- final TaskFragment adjacentFragment;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (fromFragment.getAdjacentTaskFragments().size() > 2) {
- throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs");
- }
- final TaskFragment[] tmpAdjacent = new TaskFragment[1];
- fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> {
- tmpAdjacent[0] = adjacentTF;
- return true;
- });
- adjacentFragment = tmpAdjacent[0];
- } else {
- adjacentFragment = fromFragment.getAdjacentTaskFragment();
+ if (fromFragment.getAdjacentTaskFragments().size() > 2) {
+ throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs");
}
+ final TaskFragment[] tmpAdjacent = new TaskFragment[1];
+ fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> {
+ tmpAdjacent[0] = adjacentTF;
+ return true;
+ });
+ final TaskFragment adjacentFragment = tmpAdjacent[0];
if (adjacentFragment.isIsolatedNav()) {
// Don't move the focus if the adjacent TF is isolated navigation.
return false;
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ea1f35a130b0..a012ec137892 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1671,13 +1671,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
if (!taskFragment.isAdjacentTo(secondaryTaskFragment)) {
// Only have lifecycle effect if the adjacent changed.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- // Activity Embedding only set two TFs adjacent.
- taskFragment.setAdjacentTaskFragments(
- new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment));
- } else {
- taskFragment.setAdjacentTaskFragment(secondaryTaskFragment);
- }
+ // Activity Embedding only set two TFs adjacent.
+ taskFragment.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment));
effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
@@ -2220,30 +2216,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer());
- if (wc1 == null || !wc1.isAttached()) {
- Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1);
- return TRANSACT_EFFECTS_NONE;
- }
- final TaskFragment root1 = wc1.asTaskFragment();
- final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot());
- if (wc2 == null || !wc2.isAttached()) {
- Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2);
- return TRANSACT_EFFECTS_NONE;
- }
- final TaskFragment root2 = wc2.asTaskFragment();
- if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) {
- throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by"
- + " organizer root1=" + root1 + " root2=" + root2);
- }
- if (root1.isAdjacentTo(root2)) {
- return TRANSACT_EFFECTS_NONE;
- }
- root1.setAdjacentTaskFragment(root2);
- return TRANSACT_EFFECTS_LIFECYCLE;
- }
-
final IBinder[] containers = hop.getContainers();
final ArraySet<TaskFragment> adjacentRoots = new ArraySet<>();
for (IBinder container : containers) {
diff --git a/services/core/jni/com_android_server_display_DisplayControl.cpp b/services/core/jni/com_android_server_display_DisplayControl.cpp
index aeae13d83809..c674f9037aa4 100644
--- a/services/core/jni/com_android_server_display_DisplayControl.cpp
+++ b/services/core/jni/com_android_server_display_DisplayControl.cpp
@@ -24,12 +24,13 @@
namespace android {
static jobject nativeCreateVirtualDisplay(JNIEnv* env, jclass clazz, jstring nameObj,
- jboolean secure, jstring uniqueIdStr,
- jfloat requestedRefreshRate) {
+ jboolean secure, jboolean optimizeForPower,
+ jstring uniqueIdStr, jfloat requestedRefreshRate) {
const ScopedUtfChars name(env, nameObj);
const ScopedUtfChars uniqueId(env, uniqueIdStr);
sp<IBinder> token(SurfaceComposerClient::createVirtualDisplay(std::string(name.c_str()),
bool(secure),
+ bool(optimizeForPower),
std::string(uniqueId.c_str()),
requestedRefreshRate));
return javaObjectForIBinder(env, token);
@@ -182,7 +183,7 @@ static jobject nativeGetPhysicalDisplayToken(JNIEnv* env, jclass clazz, jlong ph
static const JNINativeMethod sDisplayMethods[] = {
// clang-format off
- {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZLjava/lang/String;F)Landroid/os/IBinder;",
+ {"nativeCreateVirtualDisplay", "(Ljava/lang/String;ZZLjava/lang/String;F)Landroid/os/IBinder;",
(void*)nativeCreateVirtualDisplay },
{"nativeDestroyVirtualDisplay", "(Landroid/os/IBinder;)V",
(void*)nativeDestroyVirtualDisplay },
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0ad976c38565..51ed6bb2aa40 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -3806,9 +3806,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// Update user switcher message to activity manager.
ActivityManagerInternal activityManagerInternal =
mInjector.getActivityManagerInternal();
- activityManagerInternal.setSwitchingFromSystemUserMessage(
+ int deviceOwnerUserId = UserHandle.getUserId(deviceOwner.getUid());
+ activityManagerInternal.setSwitchingFromUserMessage(deviceOwnerUserId,
deviceOwner.startUserSessionMessage);
- activityManagerInternal.setSwitchingToSystemUserMessage(
+ activityManagerInternal.setSwitchingToUserMessage(deviceOwnerUserId,
deviceOwner.endUserSessionMessage);
}
@@ -19716,7 +19717,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
mInjector.getActivityManagerInternal()
- .setSwitchingFromSystemUserMessage(startUserSessionMessageString);
+ .setSwitchingFromUserMessage(caller.getUserId(), startUserSessionMessageString);
}
@Override
@@ -19741,7 +19742,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
mInjector.getActivityManagerInternal()
- .setSwitchingToSystemUserMessage(endUserSessionMessageString);
+ .setSwitchingToUserMessage(caller.getUserId(), endUserSessionMessageString);
}
@Override
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 1f8ccde98d35..2770caa8aaa4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -324,7 +324,8 @@ public class DisplayManagerServiceTest {
return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
new VirtualDisplayAdapter.SurfaceControlDisplayFactory() {
@Override
- public IBinder createDisplay(String name, boolean secure, String uniqueId,
+ public IBinder createDisplay(String name, boolean secure,
+ boolean optimizeForPower, String uniqueId,
float requestedRefreshRate) {
return mMockDisplayToken;
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
index 0bef3b89547f..10bea7d331cd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
@@ -416,7 +416,7 @@ public class VirtualDisplayAdapterTest {
final String uniqueId = "uniqueId";
final IBinder displayToken = new Binder();
when(mMockSufaceControlDisplayFactory.createDisplay(
- any(), anyBoolean(), eq(uniqueId), anyFloat()))
+ any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat()))
.thenReturn(displayToken);
// The display needs to be public, otherwise it will be considered never blank.
@@ -456,6 +456,49 @@ public class VirtualDisplayAdapterTest {
verify(mMockCallback).onPaused();
}
+ @EnableFlags(
+ android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE)
+ @Test
+ public void createVirtualDisplayLocked_neverBlank_optimizesForPower() {
+ final String uniqueId = "uniqueId";
+ final IBinder displayToken = new Binder();
+ final String name = "name";
+ when(mVirtualDisplayConfigMock.getName()).thenReturn(name);
+ when(mMockSufaceControlDisplayFactory.createDisplay(
+ any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat()))
+ .thenReturn(displayToken);
+
+ // Use a private display to cause the display to be never blank.
+ mAdapter.createVirtualDisplayLocked(mMockCallback,
+ /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage",
+ uniqueId, /* surface= */ mSurfaceMock, 0, mVirtualDisplayConfigMock);
+
+ verify(mMockSufaceControlDisplayFactory).createDisplay(eq(name), eq(false), eq(true),
+ eq(uniqueId), anyFloat());
+ }
+
+ @EnableFlags(
+ android.companion.virtualdevice.flags.Flags.FLAG_CORRECT_VIRTUAL_DISPLAY_POWER_STATE)
+ @Test
+ public void createVirtualDisplayLocked_blankable_optimizesForPerformance() {
+ final String uniqueId = "uniqueId";
+ final IBinder displayToken = new Binder();
+ final String name = "name";
+ when(mVirtualDisplayConfigMock.getName()).thenReturn(name);
+ when(mMockSufaceControlDisplayFactory.createDisplay(
+ any(), anyBoolean(), anyBoolean(), eq(uniqueId), anyFloat()))
+ .thenReturn(displayToken);
+
+ // Use a public display to cause the display to be blankable
+ mAdapter.createVirtualDisplayLocked(mMockCallback,
+ /* projection= */ null, /* ownerUid= */ 10, /* packageName= */ "testpackage",
+ uniqueId, /* surface= */ mSurfaceMock, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
+ mVirtualDisplayConfigMock);
+
+ verify(mMockSufaceControlDisplayFactory).createDisplay(eq(name), eq(false), eq(false),
+ eq(uniqueId), anyFloat());
+ }
+
private IVirtualDisplayCallback createCallback() {
return new IVirtualDisplayCallback.Stub() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
index 49c37f163ff2..241ffdc19ce4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
@@ -36,24 +36,29 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.internal.R;
import org.junit.AfterClass;
import org.junit.Before;
@@ -70,10 +75,11 @@ import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
+import java.util.Set;
/**
* Unit tests for the most important helpers of {@link WallpaperCropper}, in particular
- * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}.
+ * {@link WallpaperCropper#getCrop(Point, WallpaperDefaultDisplayInfo, Point, SparseArray, boolean)}.
*/
@Presubmit
@RunWith(AndroidJUnit4.class)
@@ -83,6 +89,12 @@ public class WallpaperCropperTest {
@Mock
private WallpaperDisplayHelper mWallpaperDisplayHelper;
+
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private Resources mResources;
private WallpaperCropper mWallpaperCropper;
private static final Point PORTRAIT_ONE = new Point(500, 800);
@@ -175,14 +187,21 @@ public class WallpaperCropperTest {
return tempDir;
}
- private void setUpWithDisplays(List<Point> displaySizes) {
+ private WallpaperDefaultDisplayInfo setUpWithDisplays(List<Point> displaySizes) {
mDisplaySizes = new SparseArray<>();
displaySizes.forEach(size -> {
mDisplaySizes.put(getOrientation(size), size);
Point rotated = new Point(size.y, size.x);
mDisplaySizes.put(getOrientation(rotated), rotated);
});
+ Set<WindowMetrics> windowMetrics = new ArraySet<>();
+ for (Point displaySize : displaySizes) {
+ windowMetrics.add(
+ new WindowMetrics(new Rect(0, 0, displaySize.x, displaySize.y),
+ new WindowInsets.Builder().build()));
+ }
when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())).thenReturn(windowMetrics);
if (displaySizes.size() == 2) {
Point largestDisplay = displaySizes.stream().max(
Comparator.comparingInt(p -> p.x * p.y)).get();
@@ -192,11 +211,16 @@ public class WallpaperCropperTest {
mFolded = getOrientation(smallestDisplay);
mUnfoldedRotated = getRotatedOrientation(mUnfolded);
mFoldedRotated = getRotatedOrientation(mFolded);
+ // foldable
+ doReturn(new int[]{0}).when(mResources).getIntArray(R.array.config_foldedDeviceStates);
+ } else {
+ // no foldable
+ doReturn(new int[]{}).when(mResources).getIntArray(R.array.config_foldedDeviceStates);
}
- doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0)))
- .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt());
- doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0)))
- .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt());
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ when(mWallpaperDisplayHelper.getDefaultDisplayInfo()).thenReturn(defaultDisplayInfo);
+ return defaultDisplayInfo;
}
private int getFoldedOrientation(int orientation) {
@@ -435,7 +459,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_noSuggestedCrops() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(800, 1000);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -455,8 +479,9 @@ public class WallpaperCropperTest {
for (boolean rtl : List.of(false, true)) {
Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize)
: leftOf(bitmapRect, expectedCropSize);
- assertThat(mWallpaperCropper.getCrop(
- displaySize, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(
+ displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl))
.isEqualTo(expectedCrop);
}
}
@@ -469,7 +494,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_hasSuggestedCrop() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(800, 1000);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800));
@@ -479,11 +504,13 @@ public class WallpaperCropperTest {
}
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- new Point(300, 800), bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(new Point(300, 800), defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT));
- assertThat(mWallpaperCropper.getCrop(
- new Point(500, 800), bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(new Point(500, 800), defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(new Rect(0, 0, 500, 800));
}
}
@@ -499,7 +526,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_hasRotatedSuggestedCrop() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(2000, 1800);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -510,12 +537,14 @@ public class WallpaperCropperTest {
suggestedCrops.put(ORIENTATION_PORTRAIT, centerOf(bitmapRect, portrait));
suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape));
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- landscape, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(landscape, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(centerOf(bitmapRect, landscape));
- assertThat(mWallpaperCropper.getCrop(
- squarePortrait, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(squarePortrait, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(centerOf(bitmapRect, squarePortrait));
}
}
@@ -532,7 +561,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasUnfoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2400);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
@@ -569,8 +598,9 @@ public class WallpaperCropperTest {
expectedCrop.right = Math.min(
unfoldedCrop.right, unfoldedCrop.right + maxParallax);
}
- assertThat(mWallpaperCropper.getCrop(
- foldedDisplay, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(foldedDisplay, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(expectedCrop);
}
}
@@ -588,7 +618,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasFoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
@@ -610,12 +640,14 @@ public class WallpaperCropperTest {
Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo);
for (boolean rtl : List.of(false, true)) {
- assertThat(centerOf(mWallpaperCropper.getCrop(
- unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne))
+ assertThat(centerOf(
+ WallpaperCropper.getCrop(unfoldedDisplayOne, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl), foldedDisplayOne))
.isEqualTo(foldedCropOne);
- assertThat(centerOf(mWallpaperCropper.getCrop(
- unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo))
+ assertThat(centerOf(
+ WallpaperCropper.getCrop(unfoldedDisplayTwo, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl), foldedDisplayTwo))
.isEqualTo(foldedCropTwo);
}
}
@@ -633,7 +665,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
Point largestDisplay = displaySizes.stream().max(
@@ -650,8 +682,9 @@ public class WallpaperCropperTest {
Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded);
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(rotatedFoldedDisplay, defaultDisplayInfo,
+ bitmapSize, suggestedCrops, rtl))
.isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay));
}
}
@@ -670,7 +703,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasRotatedFoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
@@ -689,8 +722,8 @@ public class WallpaperCropperTest {
Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded);
for (boolean rtl : List.of(false, true)) {
- Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop(
- rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl);
+ Rect rotatedUnfoldedCrop = WallpaperCropper.getCrop(rotatedUnfoldedDisplay,
+ defaultDisplayInfo, bitmapSize, suggestedCrops, rtl);
assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay))
.isEqualTo(rotatedFoldedCrop);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java
new file mode 100644
index 000000000000..312db91afb12
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_PORTRAIT;
+import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_SQUARE_PORTRAIT;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.wallpaper.WallpaperDefaultDisplayInfo.FoldableOrientations;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Set;
+
+/** Unit tests for {@link WallpaperDefaultDisplayInfo}. */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WallpaperDefaultDisplayInfoTest {
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private Resources mResources;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ }
+
+ @Test
+ public void defaultDisplayInfo_foldable_shouldHaveExpectedContent() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1080, 2424));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2424, 1080));
+ displaySizes.put(ORIENTATION_SQUARE_PORTRAIT, new Point(2076, 2152));
+ displaySizes.put(ORIENTATION_SQUARE_LANDSCAPE, new Point(2152, 2076));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isTrue();
+ assertThat(defaultDisplayInfo.isLargeScreen).isFalse();
+ assertThat(defaultDisplayInfo.foldableOrientations).containsExactly(
+ new FoldableOrientations(
+ /* foldedOrientation= */ ORIENTATION_PORTRAIT,
+ /* unfoldedOrientation= */ ORIENTATION_SQUARE_PORTRAIT),
+ new FoldableOrientations(
+ /* foldedOrientation= */ ORIENTATION_LANDSCAPE,
+ /* unfoldedOrientation= */ ORIENTATION_SQUARE_LANDSCAPE));
+ }
+
+ @Test
+ public void defaultDisplayInfo_tablet_shouldHaveExpectedContent() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1600, 2560));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2560, 1600));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isFalse();
+ assertThat(defaultDisplayInfo.isLargeScreen).isTrue();
+ assertThat(defaultDisplayInfo.foldableOrientations).isEmpty();
+ }
+
+ @Test
+ public void defaultDisplayInfo_phone_shouldHaveExpectedContent() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 1280, 2856);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 3f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1280, 2856));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2856, 1280));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isFalse();
+ assertThat(defaultDisplayInfo.isLargeScreen).isFalse();
+ assertThat(defaultDisplayInfo.foldableOrientations).isEmpty();
+ }
+
+ @Test
+ public void defaultDisplayInfo_equals_sameContent_shouldEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo).isEqualTo(otherDefaultDisplayInfo);
+ }
+
+ @Test
+ public void defaultDisplayInfo_equals_differentBounds_shouldNotEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ // For the first call
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics))
+ // For the second+ call
+ .thenReturn(Set.of(innerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo).isNotEqualTo(otherDefaultDisplayInfo);
+ }
+
+ @Test
+ public void defaultDisplayInfo_hashCode_sameContent_shouldEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.hashCode()).isEqualTo(otherDefaultDisplayInfo.hashCode());
+ }
+
+ @Test
+ public void defaultDisplayInfo_hashCode_differentBounds_shouldNotEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ // For the first call
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics))
+ // For the second+ call
+ .thenReturn(Set.of(innerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.hashCode()).isNotEqualTo(otherDefaultDisplayInfo.hashCode());
+ }
+
+ @Test
+ public void getFoldedOrientation_foldable_shouldReturnExpectedOrientation() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_PORTRAIT);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_LANDSCAPE);
+ // Use a folded orientation for a folded orientation should return unknown.
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getUnfoldedOrientation_foldable_shouldReturnExpectedOrientation() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_SQUARE_PORTRAIT);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_SQUARE_LANDSCAPE);
+ // Use an unfolded orientation for an unfolded orientation should return unknown.
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getFoldedOrientation_nonFoldable_shouldReturnUnknown() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getUnFoldedOrientation_nonFoldable_shouldReturnUnknown() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index 69877c372442..ea25e7992dd9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -43,6 +43,7 @@ import android.view.accessibility.AccessibilityManager;
import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.accessibility.BaseEventStreamTransformation;
import org.junit.After;
import org.junit.Before;
@@ -70,6 +71,19 @@ public class AutoclickControllerTest {
@Mock private WindowManager mMockWindowManager;
private AutoclickController mController;
+ private static class MotionEventCaptor extends BaseEventStreamTransformation {
+ public MotionEvent downEvent;
+
+ @Override
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downEvent = event;
+ break;
+ }
+ }
+ }
+
@Before
public void setUp() {
mTestableLooper = TestableLooper.get(this);
@@ -713,6 +727,100 @@ public class AutoclickControllerTest {
assertThat(mController.mAutoclickScrollPanel.isVisible()).isFalse();
}
+ @Test
+ public void sendClick_clickType_leftClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify left click sent.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_PRIMARY);
+ }
+
+ @Test
+ public void sendClick_clickType_rightClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Set click type to right click.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify right click sent.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_SECONDARY);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Set click type to right click.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+ // Set mouse to hover panel.
+ AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
+ when(mockAutoclickTypePanel.isHovered()).thenReturn(true);
+ mController.mAutoclickTypePanel = mockAutoclickTypePanel;
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify left click is sent due to the mouse hovering the panel.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_PRIMARY);
+ }
+
private void injectFakeMouseActionHoverMoveEvent() {
MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_MOUSE);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index f9946604ad5d..b842d3a42f26 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -389,6 +389,44 @@ public final class UserManagerTest {
}
@Test
+ public void testSupervisingProfile() throws Exception {
+ assumeTrue("Device doesn't support supervising profiles ",
+ mUserManager.isUserTypeEnabled(UserManager.USER_TYPE_PROFILE_SUPERVISING));
+
+ final UserTypeDetails userTypeDetails =
+ UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_SUPERVISING);
+ assertWithMessage("No supervising user type on device").that(userTypeDetails).isNotNull();
+
+
+ // Create supervising profile if it doesn't exist
+ UserInfo supervisingUser = getSupervisingProfile();
+ if (supervisingUser == null) {
+ supervisingUser = createUser("Supervising",
+ UserManager.USER_TYPE_PROFILE_SUPERVISING, /*flags*/ 0);
+ }
+ assertWithMessage("Couldn't create supervising profile").that(supervisingUser).isNotNull();
+ UserHandle supervisingHandle = supervisingUser.getUserHandle();
+
+ // Test that only one supervising profile can be created
+ final UserInfo secondSupervisingProfile =
+ createUser("Supervising", UserManager.USER_TYPE_PROFILE_SUPERVISING,
+ /*flags*/ 0);
+ assertThat(secondSupervisingProfile).isNull();
+
+ // Verify that the supervising profile doesn't have a parent
+ assertThat(mUserManager.getProfileParent(supervisingHandle.getIdentifier())).isNull();
+
+ // Make sure that the supervising profile can be started in the background, and that it
+ // is visible
+ final boolean isStarted = mActivityManager.startProfile(supervisingHandle);
+ assertWithMessage("Unable to start supervising profile").that(isStarted).isTrue();
+ final UserManager umSupervising = (UserManager) mContext.createPackageContextAsUser(
+ "android", 0, supervisingHandle).getSystemService(Context.USER_SERVICE);
+ assertWithMessage("Supervising profile not visible").that(
+ umSupervising.isUserVisible()).isTrue();
+ }
+
+ @Test
public void testGetProfileAccessibilityString_throwsExceptionForNonProfileUser() {
UserInfo user1 = createUser("Guest 1", UserInfo.FLAG_GUEST);
assertThat(user1).isNotNull();
@@ -2198,4 +2236,13 @@ public final class UserManagerTest {
assertEquals(actual.getLevel(), expected.getLevel());
}
+ @Nullable
+ private UserInfo getSupervisingProfile() {
+ for (UserInfo user : mUserManager.getUsers()) {
+ if (user.userType.equals(UserManager.USER_TYPE_PROFILE_SUPERVISING)) {
+ return user;
+ }
+ }
+ return null;
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index e3e9cc426bb3..08b0077c49b3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -797,7 +797,7 @@ public class ActivityStarterTests extends WindowTestsBase {
// Create adjacent tasks and put one activity under it
final Task parent = new TaskBuilder(mSupervisor).build();
final Task adjacentParent = new TaskBuilder(mSupervisor).build();
- parent.setAdjacentTaskFragment(adjacentParent);
+ parent.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(parent, adjacentParent));
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setParentTask(parent)
.setCreateTask(true).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index a9be47d71213..86d901b640ff 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -488,14 +488,13 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase {
WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask,
WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
- tf1.setAdjacentTaskFragment(tf2);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2));
assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
}
@Test
- @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
- Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS})
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() {
final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask,
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8fe08553db95..cb98b9a490d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -243,6 +243,11 @@ class AppCompatActivityRobot {
.getAspectRatioOverrides()).getUserMinAspectRatio();
}
+ void setShouldRefreshActivityForCameraCompat(boolean enabled) {
+ doReturn(enabled).when(mActivityStack.top().mAppCompatController.getCameraOverrides())
+ .shouldRefreshActivityForCameraCompat();
+ }
+
void setIgnoreOrientationRequest(boolean enabled) {
mDisplayContent.setIgnoreOrientationRequest(enabled);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index 05f6ed644632..7ef85262dfc2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -64,6 +64,19 @@ class AppCompatConfigurationRobot {
.isCameraCompatTreatmentEnabledAtBuildTime();
}
+ void setCameraCompatAspectRatio(float aspectRatio) {
+ doReturn(aspectRatio).when(mAppCompatConfiguration).getCameraCompatAspectRatio();
+ }
+
+ void enableCameraCompatRefresh(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatRefreshEnabled();
+ }
+
+ void enableCameraCompatRefreshCycleThroughStop(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration)
+ .isCameraCompatRefreshCycleThroughStopEnabled();
+ }
+
void enableUserAppAspectRatioFullscreen(boolean enabled) {
doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index bdee3c323549..dd3e9fcbbdaf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -343,8 +343,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
// Adjacent + no companion => unable to predict
// TF1 | TF2
- tf1.setAdjacentTaskFragment(tf2);
- tf2.setAdjacentTaskFragment(tf1);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2));
predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr,
outPrevActivities);
assertTrue(outPrevActivities.isEmpty());
@@ -393,8 +392,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
// Adjacent => predict for previous activity.
// TF2 | TF3
// TF1
- tf2.setAdjacentTaskFragment(tf3);
- tf3.setAdjacentTaskFragment(tf2);
+ tf2.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf2, tf3));
predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr,
outPrevActivities);
assertTrue(outPrevActivities.contains(prevAr));
@@ -657,8 +655,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
final TaskFragment secondaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
- primaryTf.setAdjacentTaskFragment(secondaryTf);
- secondaryTf.setAdjacentTaskFragment(primaryTf);
+ primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf));
final WindowState primaryWindow = mock(WindowState.class);
final WindowState secondaryWindow = mock(WindowState.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index f5bec04a98d5..6f959812d742 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -21,13 +21,13 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE_
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT;
+import static android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
-import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
@@ -40,18 +40,15 @@ import static android.view.Surface.ROTATION_90;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -59,13 +56,11 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
-import android.app.CameraCompatTaskInfo;
import android.app.IApplicationThread;
import android.app.WindowConfiguration.WindowingMode;
import android.app.servertransaction.RefreshCallbackItem;
import android.app.servertransaction.ResumeActivityItem;
import android.compat.testing.PlatformCompatChangeRule;
-import android.content.ComponentName;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
@@ -73,17 +68,16 @@ import android.content.res.Configuration.Orientation;
import android.graphics.Rect;
import android.hardware.camera2.CameraManager;
import android.os.Handler;
+import android.os.RemoteException;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
-import android.view.DisplayInfo;
import android.view.Surface;
import androidx.test.filters.SmallTest;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -91,6 +85,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Tests for {@link CameraCompatFreeformPolicy}.
@@ -109,30 +104,18 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests";
private static final String TEST_PACKAGE_2 = "com.test.package.two";
private static final String CAMERA_ID_1 = "camera-1";
- private AppCompatConfiguration mAppCompatConfiguration;
-
- private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
- private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
- private ActivityRecord mActivity;
-
- // TODO(b/384465100): use a robot structure.
- @Before
- public void setUp() throws Exception {
- setupAppCompatConfiguration();
- setupCameraManager();
- setupHandler();
- doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
- }
@Test
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testFeatureDisabled_cameraCompatFreeformPolicyNotCreated() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertNull(mCameraCompatFreeformPolicy);
+ robot.checkCameraCompatPolicyNotCreated();
+ });
}
@Test
@@ -140,31 +123,37 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION})
public void testIsCameraRunningAndWindowingModeEligible_disabledViaOverride_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_cameraNotRunning_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@@ -172,64 +161,76 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(true);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testIsCameraRunningAndWindowingModeEligible_freeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
public void testIsFreeformLetterboxingForCameraAllowed_optInMechanism_notOptedIn_retFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testIsFreeformLetterboxingForCameraAllowed_notOptedOut_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_cameraNotRunning_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@@ -237,519 +238,603 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testFullscreen_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- doReturn(false).when(mActivity).inFreeformWindowingMode();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ robot.setInFreeformWindowingMode(false);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertNotInCameraCompatMode();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_0);
+ public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_0);
+ public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_270);
+ public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testCameraReconnected_cameraCompatModeAndRefresh() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true,
/* lastLetterbox= */ false);
- assertActivityRefreshRequested(/* refreshRequested */ true);
- onCameraClosed(CAMERA_ID_1);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- // Activity is letterboxed from the previous configuration change.
- callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
- /* lastLetterbox= */ true);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true);
+ robot.onCameraClosed(CAMERA_ID_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ // Activity is letterboxed from the previous configuration change.
+ robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true,
+ /* lastLetterbox= */ true);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ true);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(false);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testShouldApplyCameraCompatFreeformTreatment_notOptedOut_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)
public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mActivity.info
- .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT));
- assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ false);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ robot.checkShouldRefreshActivity(/* expected= */ true,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0),
+ robot.createConfiguration(/* letterbox= */ false, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- oldConfiguration.windowConfiguration.setDisplayRotation(0);
- newConfiguration.windowConfiguration.setDisplayRotation(90);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.checkShouldRefreshActivity(/* expected= */ true,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 90),
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- oldConfiguration.windowConfiguration.setDisplayRotation(0);
- newConfiguration.windowConfiguration.setDisplayRotation(0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.checkShouldRefreshActivity(/* expected= */ false,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0),
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-
- doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides())
- .shouldRefreshActivityForCameraCompat();
+ public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().setShouldRefreshActivityForCameraCompat(false);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
- when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
- .thenReturn(false);
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.conf().enableCameraCompatRefreshCycleThroughStop(false);
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
-
- assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true,
+ /* cycleThroughStop */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
- .shouldRefreshActivityViaPauseForCameraCompat();
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.setShouldRefreshActivityViaPause(true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true,
+ /* cycleThroughStop */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
- configureActivity(SCREEN_ORIENTATION_FULL_USER);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_FULL_USER);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- final float configAspectRatio = 1.5f;
- mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final float configAspectRatio = 1.5f;
+ robot.conf().setCameraCompatAspectRatio(configAspectRatio);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(configAspectRatio,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(configAspectRatio);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- final float configAspectRatio = 1.5f;
- mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
- doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
- .isOverrideMinAspectRatioForCameraEnabled();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.conf().setCameraCompatAspectRatio(1.5f);
+ robot.setOverrideMinAspectRatioEnabled(true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() throws
- Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- // This is a portrait rotation for a device with portrait natural orientation (most common,
- // currently the only one supported).
- assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0);
+ // This is a portrait rotation for a device with portrait natural orientation (most
+ // common, currently the only one supported).
+ robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() throws
- Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_0);
-
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
-
- // This is a landscape rotation for a device with portrait natural orientation (most common,
- // currently the only one supported).
- assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90);
- }
-
- private void setupAppCompatConfiguration() {
- mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
- spyOn(mAppCompatConfiguration);
- when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled())
- .thenReturn(false);
- when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
- .thenReturn(true);
- }
-
- private void setupCameraManager() {
- final CameraManager mockCameraManager = mock(CameraManager.class);
- doAnswer(invocation -> {
- mCameraAvailabilityCallback = invocation.getArgument(1);
- return null;
- }).when(mockCameraManager).registerAvailabilityCallback(
- any(Executor.class), any(CameraManager.AvailabilityCallback.class));
-
- when(mContext.getSystemService(CameraManager.class)).thenReturn(mockCameraManager);
- }
-
- private void setupHandler() {
- final Handler handler = mDisplayContent.mWmService.mH;
- spyOn(handler);
-
- when(handler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
- invocation -> {
- ((Runnable) invocation.getArgument(0)).run();
- return null;
- });
- }
-
- private void configureActivity(@ScreenOrientation int activityOrientation) {
- configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
- }
-
- private void configureActivity(@ScreenOrientation int activityOrientation,
- @WindowingMode int windowingMode) {
- configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode);
- }
-
- private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
- @Orientation int naturalOrientation, @WindowingMode int windowingMode) {
- setupDisplayContent(naturalOrientation);
- final Task task = setupTask(windowingMode);
- setupActivity(task, activityOrientation, windowingMode);
- setupMockApplicationThread();
-
- mCameraCompatFreeformPolicy = mDisplayContent.mAppCompatCameraPolicy
- .mCameraCompatFreeformPolicy;
- }
-
- private void setupDisplayContent(@Orientation int naturalOrientation) {
- // Create a new DisplayContent so that the flag values create the camera freeform policy.
- mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayContent.getSurfaceWidth(),
- mDisplayContent.getSurfaceHeight()).build();
- mDisplayContent.setIgnoreOrientationRequest(true);
- setDisplayRotation(ROTATION_90);
- doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
- }
-
- private Task setupTask(@WindowingMode int windowingMode) {
- final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea();
- spyOn(tda);
- doReturn(true).when(tda).supportsNonResizableMultiWindow();
-
- final Task task = new TaskBuilder(mSupervisor)
- .setDisplay(mDisplayContent)
- .setWindowingMode(windowingMode)
- .build();
- task.setBounds(0, 0, 1000, 500);
- return task;
- }
-
- private void setupActivity(@NonNull Task task, @ScreenOrientation int activityOrientation,
- @WindowingMode int windowingMode) {
- mActivity = new ActivityBuilder(mAtm)
- // Set the component to be that of the test class in order to enable compat changes
- .setComponent(ComponentName.createRelative(mContext,
- com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName()))
- .setScreenOrientation(activityOrientation)
- .setResizeMode(RESIZE_MODE_RESIZEABLE)
- .setCreateTask(true)
- .setOnTop(true)
- .setTask(task)
- .build();
- mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode();
-
- spyOn(mActivity.mAppCompatController.getCameraOverrides());
- spyOn(mActivity.info);
-
- doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
- doReturn(windowingMode == WINDOWING_MODE_FREEFORM).when(mActivity)
- .inFreeformWindowingMode();
- }
-
- private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
- mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName);
- waitHandlerIdle(mDisplayContent.mWmService.mH);
- }
-
- private void onCameraClosed(@NonNull String cameraId) {
- mCameraAvailabilityCallback.onCameraClosed(cameraId);
- waitHandlerIdle(mDisplayContent.mWmService.mH);
- }
-
- private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) {
- assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity));
- }
-
- private void assertNotInCameraCompatMode() {
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE);
- }
-
- private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception {
- assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
- }
-
- private void assertActivityRefreshRequested(boolean refreshRequested,
- boolean cycleThroughStop) throws Exception {
- verify(mActivity.mAppCompatController.getCameraOverrides(),
- times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true);
-
- final RefreshCallbackItem refreshCallbackItem =
- new RefreshCallbackItem(mActivity.token, cycleThroughStop ? ON_STOP : ON_PAUSE);
- final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token,
- /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
-
- verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
- .scheduleTransactionItems(mActivity.app.getThread(),
- refreshCallbackItem, resumeActivityItem);
- }
-
- private void callOnActivityConfigurationChanging(ActivityRecord activity) {
- callOnActivityConfigurationChanging(activity, /* letterboxNew= */ true,
- /* lastLetterbox= */false);
- }
-
- private void callOnActivityConfigurationChanging(ActivityRecord activity, boolean letterboxNew,
- boolean lastLetterbox) {
- mDisplayContent.mAppCompatCameraPolicy.mActivityRefresher
- .onActivityConfigurationChanging(activity,
- /* newConfig */ createConfiguration(letterboxNew),
- /* lastReportedConfig */ createConfiguration(lastLetterbox));
- }
-
- private Configuration createConfiguration(boolean letterbox) {
- final Configuration configuration = new Configuration();
- Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ 600)
- : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600);
- configuration.windowConfiguration.setAppBounds(bounds);
- return configuration;
- }
-
- private void setDisplayRotation(@Surface.Rotation int displayRotation) {
- doAnswer(invocation -> {
- DisplayInfo displayInfo = new DisplayInfo();
- mDisplayContent.getDisplay().getDisplayInfo(displayInfo);
- displayInfo.rotation = displayRotation;
- // Set height so that the natural orientation (rotation is 0) is portrait. This is the
- // case for most standard phones and tablets.
- // TODO(b/365725400): handle landscape natural orientation.
- displayInfo.logicalHeight = displayRotation % 180 == 0 ? 800 : 600;
- displayInfo.logicalWidth = displayRotation % 180 == 0 ? 600 : 800;
- return displayInfo;
- }).when(mDisplayContent.mWmService.mDisplayManagerInternal)
- .getDisplayInfo(anyInt());
- }
-
- private void setupMockApplicationThread() {
- IApplicationThread mockApplicationThread = mock(IApplicationThread.class);
- spyOn(mActivity.app);
- doReturn(mockApplicationThread).when(mActivity.app).getThread();
- }
-
- private void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int
- expectedRotation) throws Exception {
- final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor =
- ArgumentCaptor.forClass(CompatibilityInfo.class);
- verify(mActivity.app.getThread()).updatePackageCompatibilityInfo(eq(mActivity.packageName),
- compatibilityInfoArgumentCaptor.capture());
-
- final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue();
- assertTrue(compatInfo.isOverrideDisplayRotationRequired());
- assertEquals(expectedRotation, compatInfo.applicationDisplayRotation);
+ public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
+
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ // This is a landscape rotation for a device with portrait natural orientation (most
+ // common, currently the only one supported).
+ robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90);
+ });
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<CameraCompatFreeformPolicyRobotTests> consumer) {
+ final CameraCompatFreeformPolicyRobotTests robot =
+ new CameraCompatFreeformPolicyRobotTests(mWm, mAtm, mSupervisor, this);
+ consumer.accept(robot);
+ }
+
+ private static class CameraCompatFreeformPolicyRobotTests extends AppCompatRobotBase {
+ private final WindowTestsBase mWindowTestsBase;
+
+ private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+ CameraCompatFreeformPolicyRobotTests(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor,
+ @NonNull WindowTestsBase windowTestsBase) {
+ super(wm, atm, supervisor);
+ mWindowTestsBase = windowTestsBase;
+ setupCameraManager();
+ setupAppCompatConfiguration();
+ }
+
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ if (displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy != null) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+ }
+ }
+
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ setupCameraManager();
+ setupHandler();
+ setupMockApplicationThread();
+ }
+
+ private void setupMockApplicationThread() {
+ IApplicationThread mockApplicationThread = mock(IApplicationThread.class);
+ spyOn(activity().top().app);
+ doReturn(mockApplicationThread).when(activity().top().app).getThread();
+ }
+
+ private Configuration createConfiguration(boolean letterbox, int rotation) {
+ final Configuration configuration = createConfiguration(letterbox);
+ configuration.windowConfiguration.setDisplayRotation(rotation);
+ return configuration;
+ }
+
+ private Configuration createConfiguration(boolean letterbox) {
+ final Configuration configuration = new Configuration();
+ Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/
+ 600)
+ : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600);
+ configuration.windowConfiguration.setAppBounds(bounds);
+ return configuration;
+ }
+
+ private void setupAppCompatConfiguration() {
+ applyOnConf((c) -> {
+ c.enableCameraCompatTreatment(true);
+ c.enableCameraCompatTreatmentAtBuildTime(true);
+ c.enableCameraCompatRefresh(true);
+ c.enableCameraCompatRefreshCycleThroughStop(true);
+ c.enableCameraCompatSplitScreenAspectRatio(false);
+ });
+ }
+
+ private void setupCameraManager() {
+ final CameraManager mockCameraManager = mock(CameraManager.class);
+ doAnswer(invocation -> {
+ mCameraAvailabilityCallback = invocation.getArgument(1);
+ return null;
+ }).when(mockCameraManager).registerAvailabilityCallback(
+ any(Executor.class), any(CameraManager.AvailabilityCallback.class));
+
+ doReturn(mockCameraManager).when(mWindowTestsBase.mWm.mContext).getSystemService(
+ CameraManager.class);
+ }
+
+ private void setupHandler() {
+ final Handler handler = activity().top().mWmService.mH;
+ spyOn(handler);
+
+ doAnswer(invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ }).when(handler).postDelayed(any(Runnable.class), anyLong());
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation) {
+ configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation,
+ @WindowingMode int windowingMode) {
+ configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode);
+ }
+
+ private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation, @WindowingMode int windowingMode) {
+ applyOnActivity(a -> {
+ dw().allowEnterDesktopMode(true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.setIgnoreOrientationRequest(true);
+ a.rotateDisplayForTopActivity(ROTATION_90);
+ a.configureTopActivity(/* minAspect */ -1, /* maxAspect */ -1,
+ activityOrientation, /* isUnresizable */ false);
+ a.top().setWindowingMode(windowingMode);
+ a.displayContent().setWindowingMode(windowingMode);
+ a.setDisplayNaturalOrientation(naturalOrientation);
+ spyOn(a.top().mAppCompatController.getCameraOverrides());
+ spyOn(a.top().info);
+ doReturn(a.displayContent().getDisplayInfo()).when(
+ a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo(
+ a.displayContent().mDisplayId);
+ });
+ }
+
+ private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
+ mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName);
+ waitHandlerIdle();
+ }
+
+ private void onCameraClosed(@NonNull String cameraId) {
+ mCameraAvailabilityCallback.onCameraClosed(cameraId);
+ }
+
+ private void waitHandlerIdle() {
+ mWindowTestsBase.waitHandlerIdle(activity().displayContent().mWmService.mH);
+ }
+
+ void setInFreeformWindowingMode(boolean inFreeform) {
+ doReturn(inFreeform).when(activity().top()).inFreeformWindowingMode();
+ }
+
+ void setShouldRefreshActivityViaPause(boolean enabled) {
+ doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides())
+ .shouldRefreshActivityViaPauseForCameraCompat();
+ }
+
+ void checkShouldRefreshActivity(boolean expected, Configuration newConfig,
+ Configuration oldConfig) {
+ assertEquals(expected, cameraCompatFreeformPolicy().shouldRefreshActivity(
+ activity().top(), newConfig, oldConfig));
+ }
+
+ void checkCameraCompatPolicyNotCreated() {
+ assertNull(cameraCompatFreeformPolicy());
+ }
+
+ void checkIsCameraRunningAndWindowingModeEligible(boolean expected) {
+ assertEquals(expected, cameraCompatFreeformPolicy()
+ .isCameraRunningAndWindowingModeEligible(activity().top()));
+ }
+
+ void checkIsFreeformLetterboxingForCameraAllowed(boolean expected) {
+ assertEquals(expected, cameraCompatFreeformPolicy()
+ .isFreeformLetterboxingForCameraAllowed(activity().top()));
+ }
+
+ void checkCameraCompatAspectRatioEquals(float aspectRatio) {
+ assertEquals(aspectRatio,
+ cameraCompatFreeformPolicy().getCameraCompatAspectRatio(activity().top()),
+ /* delta= */ 0.001);
+ }
+
+ private void assertInCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ assertEquals(mode, cameraCompatFreeformPolicy().getCameraCompatMode(activity().top()));
+ }
+
+ private void assertNotInCameraCompatMode() {
+ assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested) {
+ assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested,
+ boolean cycleThroughStop) {
+ verify(activity().top().mAppCompatController.getCameraOverrides(),
+ times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true);
+
+ final RefreshCallbackItem refreshCallbackItem =
+ new RefreshCallbackItem(activity().top().token,
+ cycleThroughStop ? ON_STOP : ON_PAUSE);
+ final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(
+ activity().top().token,
+ /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
+ try {
+ verify(activity().top().mAtmService.getLifecycleManager(),
+ times(refreshRequested ? 1 : 0))
+ .scheduleTransactionItems(activity().top().app.getThread(),
+ refreshCallbackItem, resumeActivityItem);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ private void callOnActivityConfigurationChanging() {
+ callOnActivityConfigurationChanging(/* letterboxNew= */ true,
+ /* lastLetterbox= */false);
+ }
+
+ private void callOnActivityConfigurationChanging(boolean letterboxNew,
+ boolean lastLetterbox) {
+ activity().displayContent().mAppCompatCameraPolicy.mActivityRefresher
+ .onActivityConfigurationChanging(activity().top(),
+ /* newConfig */ createConfiguration(letterboxNew),
+ /* lastReportedConfig */ createConfiguration(lastLetterbox));
+ }
+
+ void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) {
+ assertEquals(active,
+ cameraCompatFreeformPolicy().isTreatmentEnabledForActivity(activity().top(),
+ /* checkOrientation */ true));
+ }
+
+ void setOverrideMinAspectRatioEnabled(boolean enabled) {
+ doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides())
+ .isOverrideMinAspectRatioForCameraEnabled();
+ }
+
+ void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int
+ expectedRotation) {
+ final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor =
+ ArgumentCaptor.forClass(CompatibilityInfo.class);
+ try {
+ verify(activity().top().app.getThread()).updatePackageCompatibilityInfo(
+ eq(activity().top().packageName),
+ compatibilityInfoArgumentCaptor.capture());
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+
+ final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue();
+ assertTrue(compatInfo.isOverrideDisplayRotationRequired());
+ assertEquals(expectedRotation, compatInfo.applicationDisplayRotation);
+ }
+
+ CameraCompatFreeformPolicy cameraCompatFreeformPolicy() {
+ return activity().displayContent().mAppCompatCameraPolicy.mCameraCompatFreeformPolicy;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
index 1e91bedb5c18..43755ea3165e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
@@ -181,6 +181,7 @@ public class DesktopModeHelperTest {
assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
}
+ @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE)
@Test
public void isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() {
doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index bc37496d14a7..e87e107cd793 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE;
@@ -157,7 +158,7 @@ public class DesktopModeLaunchParamsModifierTests extends
@Test
@EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX})
- public void testReturnsContinueIfVisibleFreeformTaskExists() {
+ public void testReturnsContinueIfFreeformTaskExists() {
setupDesktopModeLaunchParamsModifier();
when(mTarget.isEnteringDesktopMode(any(), any(), any())).thenCallRealMethod();
@@ -165,7 +166,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM).build();
doReturn(existingFreeformTask.getRootActivity()).when(dc)
- .getTopMostVisibleFreeformActivity();
+ .getTopMostFreeformActivity();
final Task launchingTask = new TaskBuilder(mSupervisor).build();
launchingTask.onDisplayChanged(dc);
@@ -269,6 +270,38 @@ public class DesktopModeLaunchParamsModifierTests extends
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES})
+ public void testInheritTaskBoundsFromExistingInstanceIfClosing() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final String packageName = "com.same.package";
+ // Setup existing task.
+ final DisplayContent dc = spy(createNewDisplay());
+ final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build();
+ existingFreeformTask.setBounds(
+ /* left */ 0,
+ /* top */ 0,
+ /* right */ 500,
+ /* bottom */ 500);
+ doReturn(existingFreeformTask.getRootActivity()).when(dc)
+ .getTopMostVisibleFreeformActivity();
+ // Set up new instance of already existing task. By default multi instance is not supported
+ // so first instance will close.
+ final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName)
+ .setCreateActivity(true).build();
+ launchingTask.onDisplayChanged(dc);
+ launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
+
+ // New instance should inherit task bounds of old instance.
+ assertEquals(RESULT_DONE,
+ new CalculateRequestBuilder().setTask(launchingTask)
+ .setActivity(launchingTask.getRootActivity()).calculate());
+ assertEquals(existingFreeformTask.getBounds(), mResult.mBounds);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java
new file mode 100644
index 000000000000..1445a6982c60
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.window.flags.Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertTrue;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest WmTests:DisplayCompatTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DisplayCompatTests extends WindowTestsBase {
+
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @EnableFlags(FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testFixedMiscConfigurationWhenMovingToDisplay() {
+ // Create an app on the default display, at which point the restart menu isn't enabled.
+ final Task task = createTask(mDefaultDisplay);
+ final ActivityRecord activity = createActivityRecord(task);
+ assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+
+ // Move the app to a secondary display, and the restart menu must get enabled.
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.copyFrom(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ final DisplayContent secondaryDisplay = createNewDisplay(displayInfo);
+ task.reparent(secondaryDisplay.getDefaultTaskDisplayArea(), true);
+ assertTrue(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+
+ // Once the app gets restarted, the restart menu must be gone.
+ activity.restartProcessIfVisible();
+ assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 71e34ef220d3..3c6a89842af9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -93,7 +93,7 @@ public class InsetsPolicyTest extends WindowTestsBase {
final Task task1 = createTask(mDisplayContent);
final Task task2 = createTask(mDisplayContent);
- task1.setAdjacentTaskFragment(task2);
+ task1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(task1, task2));
final WindowState win = createAppWindow(task1, WINDOWING_MODE_MULTI_WINDOW, "app");
final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 33a48aadbd70..e2c4a1d2dfea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -572,8 +572,8 @@ public class SizeCompatTests extends WindowTestsBase {
new TestDisplayContent.Builder(mAtm, 1000, 2000).build();
final InputDevice device = new InputDevice.Builder()
.setAssociatedDisplayId(newDisplay.mDisplayId)
- .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL
- | InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL)
.build();
final InputDevice[] devices = {device};
doReturn(true).when(newDisplay.mWmService.mInputManager)
@@ -596,6 +596,7 @@ public class SizeCompatTests extends WindowTestsBase {
assertEquals(originalTouchscreen, newConfiguration.touchscreen);
assertEquals(originalNavigation, newConfiguration.navigation);
assertEquals(originalKeyboard, newConfiguration.keyboard);
+ // TODO(b/399749909): assert keyboardHidden, hardkeyboardHidden, and navigationHidden too.
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 3776b03695d5..b558fad84efa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -78,6 +78,7 @@ import android.view.SurfaceControl;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.protolog.PerfettoProtoLogImpl;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
import com.android.server.AnimationThread;
@@ -187,7 +188,10 @@ public class SystemServicesTestRule implements TestRule {
}
private void setUp() {
- ProtoLog.init(WmProtoLogGroups.values());
+ if (ProtoLog.getSingleInstance() == null) {
+ ProtoLog.init(WmProtoLogGroups.values());
+ PerfettoProtoLogImpl.waitForInitialization();
+ }
if (mOnBeforeServicesCreated != null) {
mOnBeforeServicesCreated.run();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 986532ce5897..ec83c50e95aa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -87,7 +87,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -113,7 +114,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
final Task adjacentRootTask = createTask(
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchRootTask(rootTask,
new int[]{WINDOWING_MODE_MULTI_WINDOW}, new int[]{ACTIVITY_TYPE_STANDARD});
@@ -135,7 +137,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
adjacentRootTask.mCreatedByOrganizer = true;
createActivityRecord(adjacentRootTask);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
final Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -821,7 +824,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
adjacentRootTask.mCreatedByOrganizer = true;
final Task candidateTask = createTaskInRootTask(rootTask, 0 /* userId*/);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
// Verify the launch root with candidate task
Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index ab76ae8e378a..76660bdc7355 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -784,7 +784,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
.setFragmentToken(fragmentToken2)
.build();
mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2);
- mTaskFragment.setAdjacentTaskFragment(taskFragment2);
+ mTaskFragment.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(mTaskFragment, taskFragment2));
mTransaction.clearAdjacentTaskFragments(mFragmentToken);
mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
@@ -1267,7 +1268,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
}
@Test
- public void testTaskFragmentInPip_setAdjacentTaskFragment() {
+ public void testTaskFragmentInPip_setAdjacentTaskFragments() {
setupTaskFragmentInPip();
spyOn(mWindowOrganizerController);
@@ -1279,7 +1280,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS),
any(IllegalArgumentException.class));
- verify(mTaskFragment, never()).setAdjacentTaskFragment(any());
+ verify(mTaskFragment, never()).setAdjacentTaskFragments(any());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index cc2a76dcc9f2..7c1d7fec819b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -67,7 +67,6 @@ import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Binder;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.SurfaceControl;
import android.view.View;
@@ -363,7 +362,7 @@ public class TaskFragmentTest extends WindowTestsBase {
doReturn(true).when(primaryActivity).supportsPictureInPicture();
doReturn(false).when(secondaryActivity).supportsPictureInPicture();
- primaryTf.setAdjacentTaskFragment(secondaryTf);
+ primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf));
primaryActivity.setState(RESUMED, "test");
secondaryActivity.setState(RESUMED, "test");
@@ -390,7 +389,8 @@ public class TaskFragmentTest extends WindowTestsBase {
task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragment0.setBounds(taskFragmentBounds);
- taskFragment0.setAdjacentTaskFragment(taskFragment1);
+ taskFragment0.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragment0, taskFragment1));
taskFragment0.setCompanionTaskFragment(taskFragment1);
taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder()
.setAnimationBackgroundColor(Color.GREEN)
@@ -779,7 +779,7 @@ public class TaskFragmentTest extends WindowTestsBase {
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
- tf0.setAdjacentTaskFragment(tf1);
+ tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1));
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -834,7 +834,7 @@ public class TaskFragmentTest extends WindowTestsBase {
final Task task = createTask(mDisplayContent);
final TaskFragment tf0 = createTaskFragmentWithActivity(task);
final TaskFragment tf1 = createTaskFragmentWithActivity(task);
- tf0.setAdjacentTaskFragment(tf1);
+ tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1));
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -982,7 +982,8 @@ public class TaskFragmentTest extends WindowTestsBase {
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
- taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+ taskFragmentLeft.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight));
taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -1051,8 +1052,8 @@ public class TaskFragmentTest extends WindowTestsBase {
.setParentTask(task)
.createActivityCount(1)
.build();
- taskFragmentRight.setAdjacentTaskFragment(taskFragmentLeft);
- taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+ taskFragmentRight.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight));
final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity();
final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity();
@@ -1103,7 +1104,6 @@ public class TaskFragmentTest extends WindowTestsBase {
Math.min(outConfig.screenWidthDp, outConfig.screenHeightDp));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testAdjacentSetForTaskFragments() {
final Task task = createTask(mDisplayContent);
@@ -1119,7 +1119,6 @@ public class TaskFragmentTest extends WindowTestsBase {
() -> new TaskFragment.AdjacentSet(tf0, tf1, tf2));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testSetAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1148,7 +1147,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task2.hasAdjacentTaskFragment());
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testClearAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1167,7 +1165,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task2.hasAdjacentTaskFragment());
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testRemoveFromAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1190,7 +1187,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task1.isAdjacentTo(task1));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testRemoveFromAdjacentTaskFragmentsWhenRemove() {
final Task task0 = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 724d7e7c111c..044aacc4b988 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1750,8 +1750,7 @@ public class TaskTests extends WindowTestsBase {
primary.mVisibleRequested = true;
secondary.mVisibleRequested = true;
- primary.setAdjacentTaskFragment(secondary);
- secondary.setAdjacentTaskFragment(primary);
+ primary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primary, secondary));
primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK);
doReturn(true).when(primary).shouldBoostDimmer();
task.assignChildLayers(t);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 7030d986494f..ae6144713a1d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -929,7 +929,6 @@ public class WindowOrganizerTests extends WindowTestsBase {
assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null);
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testSetAdjacentLaunchRootSet() {
final DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 57ab13ffee89..471b065aebb4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -122,7 +122,6 @@ import android.window.WindowContainerTransaction;
import com.android.internal.policy.AttributeCache;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
import org.junit.After;
@@ -289,18 +288,6 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mAtm.mWindowManager.mAppCompatConfiguration
.setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
- // Setup WallpaperController crop utils with a simple center-align strategy
- WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> {
- Rect crop = new Rect(0, 0, displaySize.x, displaySize.y);
- crop.scale(Math.min(
- ((float) bitmapSize.x) / displaySize.x,
- ((float) bitmapSize.y) / displaySize.y));
- crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2);
- return crop;
- };
- mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils);
- mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils);
-
checkDeviceSpecificOverridesNotApplied();
}
@@ -1890,7 +1877,7 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mSecondary = mService.mTaskOrganizerController.createRootTask(
display, WINDOWING_MODE_MULTI_WINDOW, null);
- mPrimary.setAdjacentTaskFragment(mSecondary);
+ mPrimary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(mPrimary, mSecondary));
display.getDefaultTaskDisplayArea().setLaunchAdjacentFlagRootTask(mSecondary);
final Rect primaryBounds = new Rect();
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 29d394201f39..ebe00782319a 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -1237,6 +1237,10 @@ public abstract class Connection extends Conferenceable {
builder.append(isLong ? " PROPERTY_IS_DOWNGRADED_CONFERENCE" : " dngrd_conf");
}
+ if ((properties & PROPERTY_CROSS_SIM) == PROPERTY_CROSS_SIM) {
+ builder.append(isLong ? " PROPERTY_CROSS_SIM" : " xsim");
+ }
+
builder.append("]");
return builder.toString();
}
diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
index 044f11d6904c..890c346ea015 100644
--- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
@@ -184,6 +184,8 @@ class BatteryControllerTests {
@get:Rule
val rule = MockitoJUnit.rule()!!
@get:Rule
+ val context = TestableContext(ApplicationProvider.getApplicationContext())
+ @get:Rule
val inputManagerRule = MockInputManagerRule()
@Mock
@@ -194,7 +196,6 @@ class BatteryControllerTests {
private lateinit var bluetoothBatteryManager: BluetoothBatteryManager
private lateinit var batteryController: BatteryController
- private lateinit var context: TestableContext
private lateinit var testLooper: TestLooper
private lateinit var devicesChangedListener: IInputDevicesChangedListener
private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
@@ -202,7 +203,6 @@ class BatteryControllerTests {
@Before
fun setup() {
- context = TestableContext(ApplicationProvider.getApplicationContext())
testLooper = TestLooper()
val inputManager = InputManager(context)
context.addMockSystemService(InputManager::class.java, inputManager)
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
index 5875520cd259..20528f23cc8c 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
@@ -23,7 +23,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.testing.AndroidTestingRunner;
@@ -60,9 +59,12 @@ public class TouchpadDebugViewControllerTests {
private static final String TAG = "TouchpadDebugViewController";
@Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
+
+ @Rule
public final MockitoRule mockito = MockitoJUnit.rule();
- private Context mContext;
private TouchpadDebugViewController mTouchpadDebugViewController;
@Mock
private InputManager mInputManagerMock;
@@ -74,8 +76,6 @@ public class TouchpadDebugViewControllerTests {
@Before
public void setup() throws Exception {
- mContext = InstrumentationRegistry.getInstrumentation().getContext();
- TestableContext mTestableContext = new TestableContext(mContext);
mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock);
Rect bounds = new Rect(0, 0, 2560, 1600);
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index 60fa52f85e34..1c366a134300 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
@@ -51,6 +50,7 @@ import com.android.server.input.TouchpadHardwareProperties;
import com.android.server.input.TouchpadHardwareState;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -70,6 +70,10 @@ public class TouchpadDebugViewTest {
private TouchpadDebugView mTouchpadDebugView;
private WindowManager.LayoutParams mWindowLayoutParams;
+ @Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
+
@Mock
WindowManager mWindowManager;
@Mock
@@ -77,14 +81,10 @@ public class TouchpadDebugViewTest {
Rect mWindowBounds;
WindowMetrics mWindowMetrics;
- TestableContext mTestableContext;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- Context context = InstrumentationRegistry.getInstrumentation().getContext();
- mTestableContext = new TestableContext(context);
-
mTestableContext.addMockSystemService(WindowManager.class, mWindowManager);
mTestableContext.addMockSystemService(InputManager.class, mInputManager);