summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp31
-rw-r--r--apex/jobscheduler/service/aconfig/job.aconfig7
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java25
-rw-r--r--core/api/current.txt18
-rw-r--r--core/api/system-current.txt1
-rw-r--r--core/java/android/app/ActivityOptions.java7
-rw-r--r--core/java/android/app/Instrumentation.java28
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java28
-rw-r--r--core/java/android/app/appfunctions/AppFunctionService.java54
-rw-r--r--core/java/android/app/appfunctions/IAppFunctionService.aidl2
-rw-r--r--core/java/android/app/assist/AssistStructure.java11
-rw-r--r--core/java/android/app/assist/flags.aconfig13
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java3
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceParams.java20
-rw-r--r--core/java/android/content/pm/ApplicationInfo.java3
-rw-r--r--core/java/android/content/pm/SharedLibraryInfo.java18
-rw-r--r--core/java/android/content/pm/flags.aconfig9
-rw-r--r--core/java/android/content/pm/multiuser.aconfig58
-rw-r--r--core/java/android/content/pm/parsing/ApkLite.java16
-rw-r--r--core/java/android/content/pm/parsing/ApkLiteParseUtils.java52
-rw-r--r--core/java/android/content/pm/parsing/PackageLite.java13
-rw-r--r--core/java/android/hardware/camera2/CameraMetadata.java10
-rw-r--r--core/java/android/hardware/camera2/CaptureRequest.java11
-rw-r--r--core/java/android/hardware/camera2/CaptureResult.java11
-rw-r--r--core/java/android/hardware/display/DisplayManagerInternal.java10
-rw-r--r--core/java/android/hardware/input/InputSettings.java98
-rw-r--r--core/java/android/hardware/input/VirtualInputDeviceConfig.java5
-rw-r--r--core/java/android/os/OWNERS7
-rw-r--r--core/java/android/os/StatsBootstrapAtomValue.aidl1
-rw-r--r--core/java/android/os/UserManager.java10
-rw-r--r--core/java/android/os/flags.aconfig16
-rw-r--r--core/java/android/permission/flags.aconfig11
-rw-r--r--core/java/android/provider/Settings.java26
-rw-r--r--core/java/android/view/SurfaceView.java34
-rw-r--r--core/java/android/view/accessibility/AccessibilityEvent.java14
-rw-r--r--core/java/android/view/accessibility/AccessibilityNodeInfo.java122
-rw-r--r--core/java/android/view/accessibility/flags/accessibility_flags.aconfig9
-rw-r--r--core/java/android/view/flags/view_flags.aconfig8
-rw-r--r--core/java/android/webkit/WebSettings.java20
-rw-r--r--core/java/android/webkit/flags.aconfig8
-rw-r--r--core/java/android/window/DesktopModeFlags.java4
-rw-r--r--core/java/android/window/OWNERS2
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig2
-rw-r--r--core/java/com/android/internal/pm/pkg/component/AconfigFlags.java69
-rw-r--r--core/java/com/android/internal/widget/NotificationProgressBar.java222
-rw-r--r--core/java/com/android/internal/widget/NotificationProgressDrawable.java97
-rw-r--r--core/java/com/android/internal/widget/PointerLocationView.java252
-rw-r--r--core/jni/android_view_WindowManagerGlobal.cpp6
-rw-r--r--core/jni/platform/host/HostRuntime.cpp2
-rw-r--r--core/proto/android/providers/settings/system.proto11
-rw-r--r--core/tests/coretests/src/android/app/ActivityManagerTest.java1
-rw-r--r--core/tests/coretests/src/android/app/activity/ActivityManagerTest.java6
-rw-r--r--core/tests/coretests/src/android/app/assist/AssistStructureTest.java14
-rw-r--r--core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java2
-rw-r--r--core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java302
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml1
-rw-r--r--libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml (renamed from libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml)0
-rw-r--r--libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt171
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt79
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java44
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt37
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt5
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt285
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt32
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt294
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt75
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt44
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java103
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java81
-rw-r--r--libs/appfunctions/api/current.txt3
-rw-r--r--libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java50
-rw-r--r--libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java13
-rw-r--r--media/java/android/media/MediaMuxer.java6
-rw-r--r--nfc/api/system-current.txt2
-rw-r--r--nfc/java/android/nfc/INfcCardEmulation.aidl2
-rw-r--r--nfc/java/android/nfc/NfcOemExtension.java9
-rw-r--r--packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java4
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt5
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt4
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt18
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt6
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt4
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt13
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt4
-rw-r--r--packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml48
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt4
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt99
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt99
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt2
-rw-r--r--packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt80
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt2
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt8
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt5
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt46
-rw-r--r--packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java17
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java124
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt41
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt55
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java40
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java77
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java20
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt4
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt10
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java145
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java36
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java33
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java285
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java14
-rw-r--r--packages/SettingsProvider/Android.bp1
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java2
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java2
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java30
-rw-r--r--packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java69
-rw-r--r--packages/Shell/AndroidManifest.xml6
-rw-r--r--packages/SystemUI/Android.bp4
-rw-r--r--packages/SystemUI/TEST_MAPPING5
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt312
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt73
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/Color.kt12
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt46
-rw-r--r--packages/SystemUI/compose/core/tests/Android.bp1
-rw-r--r--packages/SystemUI/compose/core/tests/AndroidManifest.xml5
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/theme/PlatformThemeTest.kt151
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt17
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/EnableWidgetDialog.kt5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarouselStateLoader.kt17
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt21
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt4
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt32
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt60
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt203
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt192
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt35
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt152
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt314
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt102
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt47
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt328
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt161
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java (renamed from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt9
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt814
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt54
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt117
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt25
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt15
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt35
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt174
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt)0
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java5
-rw-r--r--packages/SystemUI/res/drawable/volume_dialog_background.xml22
-rw-r--r--packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml21
-rw-r--r--packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml23
-rw-r--r--packages/SystemUI/res/drawable/volume_dialog_spacer.xml23
-rw-r--r--packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml2
-rw-r--r--packages/SystemUI/res/layout-land-television/volume_dialog.xml135
-rw-r--r--packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml92
-rw-r--r--packages/SystemUI/res/layout-land-television/volume_dialog_row.xml4
-rw-r--r--packages/SystemUI/res/layout-land/volume_dialog.xml181
-rw-r--r--packages/SystemUI/res/layout-land/volume_dialog_legacy.xml146
-rw-r--r--packages/SystemUI/res/layout/audio_sharing_dialog.xml115
-rw-r--r--packages/SystemUI/res/layout/volume_dialog.xml154
-rw-r--r--packages/SystemUI/res/layout/volume_dialog_legacy.xml145
-rw-r--r--packages/SystemUI/res/layout/volume_dialog_slider.xml27
-rw-r--r--packages/SystemUI/res/layout/volume_dialog_slider_floating.xml24
-rw-r--r--packages/SystemUI/res/values/dimens.xml24
-rw-r--r--packages/SystemUI/res/values/strings.xml13
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java4
-rw-r--r--packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt101
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt152
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt109
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt178
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt120
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt97
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt259
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt45
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt128
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt97
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt115
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt187
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt102
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt72
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt117
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerDistanceBasedGestureMonitor.kt63
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt (renamed from packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt)6
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogVisibilityModel.kt (renamed from packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt)17
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt122
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt65
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogResourcesViewModel.kt68
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt115
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java19
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt34
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java30
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt185
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt193
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt118
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt142
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt259
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt183
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt33
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt351
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt20
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt160
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt158
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt69
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt97
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt120
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt38
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt50
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt65
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt29
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt)0
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt)11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt70
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt25
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt30
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt37
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt27
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt31
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt2
-rw-r--r--ravenwood/Android.bp2
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java43
-rw-r--r--ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java84
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java6
-rw-r--r--services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java64
-rw-r--r--services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java10
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java188
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java27
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java27
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java29
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java14
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java59
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java62
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java32
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java22
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java100
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java2
-rw-r--r--services/core/java/com/android/server/am/ProcessList.java94
-rw-r--r--services/core/java/com/android/server/appop/AppOpsService.java33
-rw-r--r--services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java17
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java20
-rw-r--r--services/core/java/com/android/server/audio/LoudnessCodecHelper.java32
-rw-r--r--services/core/java/com/android/server/compat/PlatformCompat.java15
-rw-r--r--services/core/java/com/android/server/compat/platform_compat_flags.aconfig10
-rw-r--r--services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java8
-rw-r--r--services/core/java/com/android/server/display/DisplayDeviceConfig.java14
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java14
-rw-r--r--services/core/java/com/android/server/display/ExternalDisplayPolicy.java48
-rw-r--r--services/core/java/com/android/server/display/LogicalDisplayMapper.java8
-rw-r--r--services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java20
-rw-r--r--services/core/java/com/android/server/display/feature/DisplayManagerFlags.java14
-rw-r--r--services/core/java/com/android/server/display/feature/display_flags.aconfig11
-rw-r--r--services/core/java/com/android/server/input/InputSettingsObserver.java16
-rw-r--r--services/core/java/com/android/server/input/NativeInputManagerService.java10
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java17
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubService.java13
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java575
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubTransactionManagerOld.java475
-rw-r--r--services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java72
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java33
-rw-r--r--services/core/java/com/android/server/os/NativeTombstoneManager.java5
-rw-r--r--services/core/java/com/android/server/pm/ComputerEngine.java23
-rw-r--r--services/core/java/com/android/server/pm/InstallRequest.java5
-rw-r--r--services/core/java/com/android/server/pm/PackageInstallerSession.java46
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java34
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java36
-rw-r--r--services/core/java/com/android/server/search/SearchManagerService.java12
-rw-r--r--services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java3
-rw-r--r--services/core/java/com/android/server/wm/ActivityMetricsLogger.java2
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java7
-rw-r--r--services/core/java/com/android/server/wm/ActivityStartController.java5
-rw-r--r--services/core/java/com/android/server/wm/ConfigurationContainer.java12
-rw-r--r--services/core/java/com/android/server/wm/ContentRecorder.java7
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java2
-rw-r--r--services/core/java/com/android/server/wm/Transition.java59
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java4
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp57
-rw-r--r--services/core/jni/com_android_server_utils_AnrTimer.cpp3
-rw-r--r--services/core/xsd/display-device-config/display-device-config.xsd2
-rw-r--r--services/core/xsd/display-device-config/schema/current.txt4
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java206
-rw-r--r--services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt5
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java4
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java93
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java59
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java10
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java1
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java72
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java65
-rw-r--r--services/tests/servicestests/AndroidManifest.xml1
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt21
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java1
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java1
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java25
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java26
-rw-r--r--services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java205
-rw-r--r--services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java84
-rw-r--r--services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java9
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java219
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java22
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java10
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java68
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java5
-rw-r--r--telephony/java/android/telephony/satellite/SatelliteManager.java11
-rw-r--r--telephony/java/com/android/internal/telephony/ITelephony.aidl6
-rw-r--r--tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt4
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml14
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java6
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java28
-rw-r--r--tests/testables/Android.bp4
-rw-r--r--tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt159
-rw-r--r--tests/testables/tests/Android.bp4
-rw-r--r--tests/testables/tests/goldens/recordMotion_withAnimator.json64
-rw-r--r--tests/testables/tests/goldens/recordMotion_withSpring.json48
-rw-r--r--tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt129
532 files changed, 16346 insertions, 4928 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index fbe4905d9754..c6ce799f0a24 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -24,6 +24,7 @@ aconfig_declarations_group {
"android-sdk-flags-java",
"android.adaptiveauth.flags-aconfig-java",
"android.app.appfunctions.flags-aconfig-java",
+ "android.app.assist.flags-aconfig-java",
"android.app.contextualsearch.flags-aconfig-java",
"android.app.flags-aconfig-java",
"android.app.jank.flags-aconfig-java",
@@ -64,6 +65,7 @@ aconfig_declarations_group {
"android.server.app.flags-aconfig-java",
"android.service.autofill.flags-aconfig-java",
"android.service.chooser.flags-aconfig-java",
+ "android.service.compat.flags-aconfig-java",
"android.service.controls.flags-aconfig-java",
"android.service.dreams.flags-aconfig-java",
"android.service.notification.flags-aconfig-java",
@@ -862,6 +864,21 @@ java_aconfig_library {
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+aconfig_declarations {
+ name: "android.service.compat.flags-aconfig",
+ package: "com.android.server.compat",
+ container: "system",
+ srcs: [
+ "services/core/java/com/android/server/compat/*.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "android.service.compat.flags-aconfig-java",
+ aconfig_declarations: "android.service.compat.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Multi user
aconfig_declarations {
name: "android.multiuser.flags-aconfig",
@@ -1230,6 +1247,20 @@ java_aconfig_library {
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+// Assist
+aconfig_declarations {
+ name: "android.app.assist.flags-aconfig",
+ package: "android.app.assist.flags",
+ container: "system",
+ srcs: ["core/java/android/app/assist/flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.app.assist.flags-aconfig-java",
+ aconfig_declarations: "android.app.assist.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Smartspace
aconfig_declarations {
name: "android.app.smartspace.flags-aconfig",
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index e5389b4f96fb..11c5b51e23ae 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -75,3 +75,10 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enforce_quota_policy_to_fgs_jobs"
+ namespace: "backstage_power"
+ description: "Applies the normal quota policy to FGS jobs"
+ bug: "341201311"
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index a1c72fb4c06c..03a3a0d51891 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -99,10 +99,10 @@ import java.util.function.Predicate;
* the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
* not be allowed to run more than 20 jobs within the past 10 minutes.
*
- * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
- * freely when an app enters the foreground state and are restricted when the app leaves the
- * foreground state. However, jobs that are started while the app is in the TOP state do not count
- * towards any quota and are not restricted regardless of the app's state change.
+ * Jobs are throttled while an app is not in a TOP or BOUND_TOP state. All jobs are allowed to run
+ * freely when an app enters the TOP or BOUND_TOP state and are restricted when the app leaves those
+ * states. However, jobs that are started while the app is in the TOP state do not count towards any
+ * quota and are not restricted regardless of the app's state change.
*
* Jobs will not be throttled when the device is charging. The device is considered to be charging
* once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
@@ -567,6 +567,11 @@ public final class QuotaController extends StateController {
ActivityManager.getService().registerUidObserver(new QcUidObserver(),
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
+ if (Flags.enforceQuotaPolicyToFgsJobs()) {
+ ActivityManager.getService().registerUidObserver(new QcUidObserver(),
+ ActivityManager.UID_OBSERVER_PROCSTATE,
+ ActivityManager.PROCESS_STATE_BOUND_TOP, null);
+ }
ActivityManager.getService().registerUidObserver(new QcUidObserver(),
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_TOP, null);
@@ -2706,6 +2711,12 @@ public final class QuotaController extends StateController {
}
}
+ @VisibleForTesting
+ int getProcessStateQuotaFreeThreshold() {
+ return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP :
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ }
+
private class QcHandler extends Handler {
QcHandler(Looper looper) {
@@ -2832,15 +2843,15 @@ public final class QuotaController extends StateController {
mTopAppCache.put(uid, true);
mTopAppGraceCache.delete(uid);
if (mForegroundUids.get(uid)) {
- // Went from FGS to TOP. We don't need to reprocess timers or
- // jobs.
+ // Went from a process state with quota free to TOP. We don't
+ // need to reprocess timers or jobs.
break;
}
mForegroundUids.put(uid, true);
isQuotaFree = true;
} else {
final boolean reprocess;
- if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ if (procState <= getProcessStateQuotaFreeThreshold()) {
reprocess = !mForegroundUids.get(uid);
mForegroundUids.put(uid, true);
isQuotaFree = true;
diff --git a/core/api/current.txt b/core/api/current.txt
index 7a07d582ed32..a131ea7d29a7 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4950,7 +4950,7 @@ package android.app {
field @Deprecated @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOWED = 1; // 0x1
field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3; // 0x3
field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE = 4; // 0x4
- field @FlaggedApi("com.android.window.flags.bal_additional_start_modes") public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2
+ field public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2; // 0x2
field public static final int MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED = 0; // 0x0
}
@@ -8792,7 +8792,8 @@ package android.app.appfunctions {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
- method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
@@ -12289,6 +12290,7 @@ package android.content.pm {
method public int getMemtagMode();
method public int getNativeHeapZeroInitialized();
method public int getRequestRawExternalStorageAccess();
+ method @FlaggedApi("android.content.pm.audio_playback_capture_allowance") public boolean isAudioPlaybackCaptureAllowed();
method public boolean isProfileable();
method public boolean isProfileableByShell();
method public boolean isResourceOverlay();
@@ -52577,10 +52579,12 @@ package android.view {
ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int);
ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int, int);
method public void applyTransactionToFrame(@NonNull android.view.SurfaceControl.Transaction);
+ method @FlaggedApi("android.view.flags.surface_view_set_composition_order") public int getCompositionOrder();
method public android.view.SurfaceHolder getHolder();
method @Deprecated @Nullable public android.os.IBinder getHostToken();
method public android.view.SurfaceControl getSurfaceControl();
method public void setChildSurfacePackage(@NonNull android.view.SurfaceControlViewHost.SurfacePackage);
+ method @FlaggedApi("android.view.flags.surface_view_set_composition_order") public void setCompositionOrder(int);
method @FlaggedApi("com.android.graphics.hwui.flags.limited_hdr") public void setDesiredHdrHeadroom(@FloatRange(from=0.0f, to=10000.0) float);
method public void setSecure(boolean);
method public void setSurfaceLifecycle(int);
@@ -54918,6 +54922,7 @@ package android.view.accessibility {
method public void setPackageName(CharSequence);
method public void setSpeechStateChangeTypes(int);
method public void writeToParcel(android.os.Parcel, int);
+ field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CONTENT_CHANGE_TYPE_CHECKED = 8192; // 0x2000
field public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 4; // 0x4
field public static final int CONTENT_CHANGE_TYPE_CONTENT_INVALID = 1024; // 0x400
field public static final int CONTENT_CHANGE_TYPE_DRAG_CANCELLED = 512; // 0x200
@@ -55063,6 +55068,7 @@ package android.view.accessibility {
method @Deprecated public void getBoundsInParent(android.graphics.Rect);
method public void getBoundsInScreen(android.graphics.Rect);
method public void getBoundsInWindow(@NonNull android.graphics.Rect);
+ method @FlaggedApi("android.view.accessibility.tri_state_checked") public int getChecked();
method public android.view.accessibility.AccessibilityNodeInfo getChild(int);
method @Nullable public android.view.accessibility.AccessibilityNodeInfo getChild(int, int);
method public int getChildCount();
@@ -55105,7 +55111,7 @@ package android.view.accessibility {
method public boolean isAccessibilityDataSensitive();
method public boolean isAccessibilityFocused();
method public boolean isCheckable();
- method public boolean isChecked();
+ method @Deprecated @FlaggedApi("android.view.accessibility.tri_state_checked") public boolean isChecked();
method public boolean isClickable();
method public boolean isContentInvalid();
method public boolean isContextClickable();
@@ -55150,7 +55156,8 @@ package android.view.accessibility {
method public void setBoundsInWindow(@NonNull android.graphics.Rect);
method public void setCanOpenPopup(boolean);
method public void setCheckable(boolean);
- method public void setChecked(boolean);
+ method @Deprecated @FlaggedApi("android.view.accessibility.tri_state_checked") public void setChecked(boolean);
+ method @FlaggedApi("android.view.accessibility.tri_state_checked") public void setChecked(int);
method public void setClassName(CharSequence);
method public void setClickable(boolean);
method public void setCollectionInfo(android.view.accessibility.AccessibilityNodeInfo.CollectionInfo);
@@ -55246,6 +55253,9 @@ package android.view.accessibility {
field public static final int ACTION_SELECT = 4; // 0x4
field public static final int ACTION_SET_SELECTION = 131072; // 0x20000
field public static final int ACTION_SET_TEXT = 2097152; // 0x200000
+ field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_FALSE = 0; // 0x0
+ field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_PARTIAL = 2; // 0x2
+ field @FlaggedApi("android.view.accessibility.tri_state_checked") public static final int CHECKED_STATE_TRUE = 1; // 0x1
field @NonNull public static final android.os.Parcelable.Creator<android.view.accessibility.AccessibilityNodeInfo> CREATOR;
field public static final String EXTRA_DATA_RENDERING_INFO_KEY = "android.view.accessibility.extra.DATA_RENDERING_INFO_KEY";
field public static final String EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH = "android.view.accessibility.extra.DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 8edfc21036ad..7781f881b86f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -18559,6 +18559,7 @@ package android.webkit {
method @Deprecated public abstract void setUserAgent(int);
method public abstract void setVideoOverlayForEmbeddedEncryptedVideoEnabled(boolean);
field public static final long ENABLE_SIMPLIFIED_DARK_MODE = 214741472L; // 0xcccb1e0L
+ field @FlaggedApi("android.webkit.user_agent_reduction") public static final long ENABLE_USER_AGENT_REDUCTION = 371034303L; // 0x161d88bfL
}
public class WebStorage {
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 6ab39b028032..832c88a795e5 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -120,7 +120,7 @@ public class ActivityOptions extends ComponentOptions {
/**
* Grants the {@link PendingIntent} background activity start privileges.
*
- * This behaves the same as {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOWED_ALWAYS}, except it
+ * This behaves the same as {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS}, except it
* does not grant background activity launch permissions based on the privileged permission
* <code>START_ACTIVITIES_FROM_BACKGROUND</code>.
*
@@ -136,7 +136,6 @@ public class ActivityOptions extends ComponentOptions {
/**
* Denies the {@link PendingIntent} any background activity start privileges.
*/
- @FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES)
public static final int MODE_BACKGROUND_ACTIVITY_START_DENIED = 2;
/**
* Grants the {@link PendingIntent} all background activity start privileges, including
@@ -146,12 +145,12 @@ public class ActivityOptions extends ComponentOptions {
* <p><b>Caution:</b> This mode should be used sparingly. Most apps should use
* {@link #MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE} instead, relying on notifications
* or foreground services for background interactions to minimize user disruption. However,
- * this mode is necessary for specific use cases, such as companion apps responding to
+ * this mode is necessary for specific use cases, such as companion apps responding to
* prompts from a connected device.
*
* <p>For more information on background activity start restrictions, see:
* <a href="https://developer.android.com/guide/components/activities/background-starts">
- * Restrictions on starting activities from the background</a>
+ * Restrictions on starting activities from the background</a>
*/
@FlaggedApi(Flags.FLAG_BAL_ADDITIONAL_START_MODES)
public static final int MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS = 3;
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 93a9489849af..7eacaac29d4b 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -48,6 +48,9 @@ import android.os.SystemProperties;
import android.os.TestLooperManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.ravenwood.annotation.RavenwoodKeep;
+import android.ravenwood.annotation.RavenwoodKeepPartialClass;
+import android.ravenwood.annotation.RavenwoodReplace;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.Display;
@@ -80,7 +83,7 @@ import java.util.concurrent.TimeoutException;
* implementation is described to the system through an AndroidManifest.xml's
* &lt;instrumentation&gt; tag.
*/
-@android.ravenwood.annotation.RavenwoodKeepPartialClass
+@RavenwoodKeepPartialClass
public class Instrumentation {
/**
@@ -136,7 +139,7 @@ public class Instrumentation {
private UiAutomation mUiAutomation;
private final Object mAnimationCompleteLock = new Object();
- @android.ravenwood.annotation.RavenwoodKeep
+ @RavenwoodKeep
public Instrumentation() {
}
@@ -147,7 +150,7 @@ public class Instrumentation {
* reflection, but it will serve as noticeable discouragement from
* doing such a thing.
*/
- @android.ravenwood.annotation.RavenwoodKeep
+ @RavenwoodKeep
private void checkInstrumenting(String method) {
// Check if we have an instrumentation context, as init should only get called by
// the system in startup processes that are being instrumented.
@@ -162,7 +165,7 @@ public class Instrumentation {
*
* @hide
*/
- @android.ravenwood.annotation.RavenwoodKeep
+ @RavenwoodKeep
public boolean isInstrumenting() {
// Check if we have an instrumentation context, as init should only get called by
// the system in startup processes that are being instrumented.
@@ -326,7 +329,7 @@ public class Instrumentation {
*
* @see #getTargetContext
*/
- @android.ravenwood.annotation.RavenwoodKeep
+ @RavenwoodKeep
public Context getContext() {
return mInstrContext;
}
@@ -351,7 +354,7 @@ public class Instrumentation {
*
* @see #getContext
*/
- @android.ravenwood.annotation.RavenwoodKeep
+ @RavenwoodKeep
public Context getTargetContext() {
return mAppContext;
}
@@ -2407,10 +2410,11 @@ public class Instrumentation {
*
* @hide
*/
- @android.ravenwood.annotation.RavenwoodKeep
- public final void basicInit(Context instrContext, Context appContext) {
+ @RavenwoodKeep
+ public final void basicInit(Context instrContext, Context appContext, UiAutomation ui) {
mInstrContext = instrContext;
mAppContext = appContext;
+ mUiAutomation = ui;
}
/** @hide */
@@ -2501,6 +2505,7 @@ public class Instrumentation {
*
* @see UiAutomation
*/
+ @RavenwoodKeep
public UiAutomation getUiAutomation() {
return getUiAutomation(0);
}
@@ -2539,6 +2544,7 @@ public class Instrumentation {
*
* @see UiAutomation
*/
+ @RavenwoodReplace
public UiAutomation getUiAutomation(@UiAutomationFlags int flags) {
boolean mustCreateNewAutomation = (mUiAutomation == null) || (mUiAutomation.isDestroyed());
@@ -2569,11 +2575,15 @@ public class Instrumentation {
return null;
}
+ private UiAutomation getUiAutomation$ravenwood(@UiAutomationFlags int flags) {
+ return mUiAutomation;
+ }
+
/**
* Takes control of the execution of messages on the specified looper until
* {@link TestLooperManager#release} is called.
*/
- @android.ravenwood.annotation.RavenwoodKeep
+ @RavenwoodKeep
public TestLooperManager acquireLooperManager(Looper looper) {
checkInstrumenting("acquireLooperManager");
return new TestLooperManager(looper);
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 9be928f7efd0..102540c010ae 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -470,16 +470,9 @@ public class DevicePolicyManager {
* that the user backed-out of provisioning or some precondition for provisioning wasn't met.
*
* <p>If a <a href="#roleholder">device policy management role holder</a> updater is present on
- * the device, an internet connection attempt must be made prior to launching this intent. If
- * an internet connection can not be established, provisioning will fail unless {@link
- * #EXTRA_PROVISIONING_ALLOW_OFFLINE} is explicitly set to {@code true}, in which case
- * provisioning will continue without using the
- * <a href="#roleholder">device policy management role holder</a>. If an internet connection
- * has been established, the <a href="#roleholder">device policy management role holder</a>
- * updater will be launched, which may update the
- * <a href="#roleholder">device policy management role holder</a> before continuing
- * provisioning.
+ * the device, an internet connection attempt must be made prior to launching this intent.
*/
+ // See b/365955253 for additional behaviours of this API.
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PROVISION_MANAGED_PROFILE
= "android.app.action.PROVISION_MANAGED_PROFILE";
@@ -960,23 +953,8 @@ public class DevicePolicyManager {
* A boolean extra indicating whether offline provisioning should be used.
*
* <p>The default value is {@code false}.
- *
- * <p>Usually during the <a href="#managedprovisioning">provisioning flow</a>, there will be
- * an attempt to download and install the latest version of the <a href="#roleholder">device
- * policy management role holder</a>. The platform will then
- * delegate provisioning to the <a href="#roleholder">device
- * * policy management role holder</a>.
- *
- * <p>When this extra is set to {@code true}, the
- * <a href="#managedprovisioning">provisioning flow</a> will always be handled by the platform
- * and the <a href="#roleholder">device policy management role holder</a>'s part skipped.
- *
- * <p>On Android versions prior to {@link Build.VERSION_CODES#TIRAMISU}, when this extra is
- * {@code false}, the <a href="#managedprovisioning">provisioning flow</a> will enforce that an
- * internet connection is established, or otherwise fail. When this extra is {@code true}, a
- * connection will still be attempted but when it cannot be established provisioning will
- * continue offline.
*/
+ // See b/365955253 for detailed behaviours of this API.
public static final String EXTRA_PROVISIONING_ALLOW_OFFLINE =
"android.app.extra.PROVISIONING_ALLOW_OFFLINE";
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index 7a68a656564b..ceca850a1037 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -29,11 +29,9 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
-import android.os.Bundle;
+import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.ICancellationSignal;
-import android.os.CancellationSignal;
-import android.os.RemoteCallback;
import android.os.RemoteException;
import android.util.Log;
@@ -80,6 +78,7 @@ public abstract class AppFunctionService extends Service {
*/
void perform(
@NonNull ExecuteAppFunctionRequest request,
+ @NonNull String callingPackage,
@NonNull CancellationSignal cancellationSignal,
@NonNull Consumer<ExecuteAppFunctionResponse> callback);
}
@@ -92,6 +91,7 @@ public abstract class AppFunctionService extends Service {
@Override
public void executeAppFunction(
@NonNull ExecuteAppFunctionRequest request,
+ @NonNull String callingPackage,
@NonNull ICancellationCallback cancellationCallback,
@NonNull IExecuteAppFunctionCallback callback) {
if (context.checkCallingPermission(BIND_APP_FUNCTION_SERVICE)
@@ -103,6 +103,7 @@ public abstract class AppFunctionService extends Service {
try {
onExecuteFunction.perform(
request,
+ callingPackage,
buildCancellationSignal(cancellationCallback),
safeCallback::onResult);
} catch (Exception ex) {
@@ -128,12 +129,11 @@ public abstract class AppFunctionService extends Service {
throw e.rethrowFromSystemServer();
}
- return cancellationSignal ;
+ return cancellationSignal;
}
- private final Binder mBinder = createBinder(
- AppFunctionService.this,
- AppFunctionService.this::onExecuteFunction);
+ private final Binder mBinder =
+ createBinder(AppFunctionService.this, AppFunctionService.this::onExecuteFunction);
@NonNull
@Override
@@ -141,7 +141,6 @@ public abstract class AppFunctionService extends Service {
return mBinder;
}
-
/**
* Called by the system to execute a specific app function.
*
@@ -161,7 +160,6 @@ public abstract class AppFunctionService extends Service {
*
* @param request The function execution request.
* @param callback A callback to report back the result.
- *
* @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
* Consumer)} instead. This method will be removed once usage references are updated.
*/
@@ -198,12 +196,50 @@ public abstract class AppFunctionService extends Service {
* @param request The function execution request.
* @param cancellationSignal A signal to cancel the execution.
* @param callback A callback to report back the result.
+ * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String,
+ * CancellationSignal, Consumer)} instead. This method will be removed once usage references
+ * are updated.
*/
@MainThread
+ @Deprecated
public void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull CancellationSignal cancellationSignal,
@NonNull Consumer<ExecuteAppFunctionResponse> callback) {
onExecuteFunction(request, callback);
}
+
+ /**
+ * Called by the system to execute a specific app function.
+ *
+ * <p>This method is triggered when the system requests your AppFunctionService to handle a
+ * particular function you have registered and made available.
+ *
+ * <p>To ensure proper routing of function requests, assign a unique identifier to each
+ * function. This identifier doesn't need to be globally unique, but it must be unique within
+ * your app. For example, a function to order food could be identified as "orderFood". In most
+ * cases this identifier should come from the ID automatically generated by the AppFunctions
+ * SDK. You can determine the specific function to invoke by calling {@link
+ * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+ *
+ * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+ * thread and dispatch the result with the given callback. You should always report back the
+ * result using the callback, no matter if the execution was successful or not.
+ *
+ * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
+ * the execution of function if requested by the system.
+ *
+ * @param request The function execution request.
+ * @param callingPackage The package name of the app that is requesting the execution.
+ * @param cancellationSignal A signal to cancel the execution.
+ * @param callback A callback to report back the result.
+ */
+ @MainThread
+ public void onExecuteFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull String callingPackage,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ onExecuteFunction(request, cancellationSignal, callback);
+ }
}
diff --git a/core/java/android/app/appfunctions/IAppFunctionService.aidl b/core/java/android/app/appfunctions/IAppFunctionService.aidl
index 291f33ccb1b8..bf935d2a102b 100644
--- a/core/java/android/app/appfunctions/IAppFunctionService.aidl
+++ b/core/java/android/app/appfunctions/IAppFunctionService.aidl
@@ -34,11 +34,13 @@ oneway interface IAppFunctionService {
* Called by the system to execute a specific app function.
*
* @param request the function execution request.
+ * @param callingPackage The package name of the app that is requesting the execution.
* @param cancellationCallback a callback to send back the cancellation transport.
* @param callback a callback to report back the result.
*/
void executeAppFunction(
in ExecuteAppFunctionRequest request,
+ in String callingPackage,
in ICancellationCallback cancellationCallback,
in IExecuteAppFunctionCallback callback
);
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index 508077ed43cc..1af2437a5d6a 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -1,5 +1,6 @@
package android.app.assist;
+import static android.app.assist.flags.Flags.addPlaceholderViewForNullChild;
import static android.credentials.Constants.FAILURE_CREDMAN_SELECTOR;
import static android.credentials.Constants.SUCCESS_CREDMAN_SELECTOR;
import static android.service.autofill.Flags.FLAG_AUTOFILL_CREDMAN_DEV_INTEGRATION;
@@ -284,12 +285,18 @@ public class AssistStructure implements Parcelable {
mCurViewStackEntry = entry;
}
- void writeView(ViewNode child, Parcel out, PooledStringWriter pwriter, int levelAdj) {
+ void writeView(@Nullable ViewNode child, Parcel out, PooledStringWriter pwriter,
+ int levelAdj) {
if (DEBUG_PARCEL) Log.d(TAG, "write view: at " + out.dataPosition()
+ ", windows=" + mNumWrittenWindows
+ ", views=" + mNumWrittenViews
+ ", level=" + (mCurViewStackPos+levelAdj));
out.writeInt(VALIDATE_VIEW_TOKEN);
+ if (addPlaceholderViewForNullChild() && child == null) {
+ if (DEBUG_PARCEL_TREE) Log.d(TAG, "Detected an empty child"
+ + "; writing a placeholder for the child.");
+ child = new ViewNode();
+ }
int flags = child.writeSelfToParcel(out, pwriter, mSanitizeOnWrite,
mTmpMatrix, /*willWriteChildren=*/true);
mNumWrittenViews++;
@@ -2545,7 +2552,7 @@ public class AssistStructure implements Parcelable {
ensureData();
}
Log.i(TAG, "Task id: " + mTaskId);
- Log.i(TAG, "Activity: " + (mActivityComponent != null
+ Log.i(TAG, "Activity: " + (mActivityComponent != null
? mActivityComponent.flattenToShortString()
: null));
Log.i(TAG, "Sanitize on write: " + mSanitizeOnWrite);
diff --git a/core/java/android/app/assist/flags.aconfig b/core/java/android/app/assist/flags.aconfig
new file mode 100644
index 000000000000..bf0aeacbc7f3
--- /dev/null
+++ b/core/java/android/app/assist/flags.aconfig
@@ -0,0 +1,13 @@
+package: "android.app.assist.flags"
+container: "system"
+
+flag {
+ name: "add_placeholder_view_for_null_child"
+ namespace: "machine_learning"
+ description: "Flag to add a placeholder view when a child view is null."
+ bug: "369503426"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 96700a9a3c18..70211bfd77b1 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -1135,8 +1135,11 @@ public final class VirtualDeviceManager {
/**
* Sets the visibility of the pointer icon for this VirtualDevice's associated displays.
*
+ * <p>Only applicable to trusted displays.</p>
+ *
* @param showPointerIcon True if the pointer should be shown; false otherwise. The default
* visibility is true.
+ * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setShowPointerIcon(boolean showPointerIcon) {
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 03b72bdb8823..65f9cbefb052 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,7 @@ public final class VirtualDeviceParams implements Parcelable {
* @hide
*/
@IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
- POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA,
+ POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CLIPBOARD, POLICY_TYPE_CAMERA,
POLICY_TYPE_BLOCKED_ACTIVITY})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@@ -220,11 +220,16 @@ public final class VirtualDeviceParams implements Parcelable {
* Tells the activity manager how to handle recents entries for activities run on this device.
*
* <ul>
- * <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on VirtualDisplays owned by this
+ * <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on trusted displays owned by this
* device will appear in the host device recents.
- * <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on VirtualDisplays owned by this
+ * <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on trusted displays owned by this
* device will not appear in recents.
* </ul>
+ *
+ * <p>Activities launched on untrusted displays will always show in the host device recents,
+ * regardless of the policy.</p>
+ *
+ * @see android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
*/
public static final int POLICY_TYPE_RECENTS = 2;
@@ -254,8 +259,10 @@ public final class VirtualDeviceParams implements Parcelable {
* not shared with other devices' clipboards, including the clipboard of the default device.
* <li>{@link #DEVICE_POLICY_CUSTOM}: The device's clipboard is shared with the default
* device's clipboard. Any clipboard operation on the virtual device is as if it was done on
- * the default device.
+ * the default device. Requires all displays of the virtual device to be trusted.
* </ul>
+ *
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
*/
@FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD)
public static final int POLICY_TYPE_CLIPBOARD = 4;
@@ -821,8 +828,8 @@ public final class VirtualDeviceParams implements Parcelable {
}
/**
- * Specifies a component to be used as input method on all displays owned by this virtual
- * device.
+ * Specifies a component to be used as input method on all trusted displays owned by this
+ * virtual device.
*
* @param inputMethodComponent The component name to be used as input method. Must comply to
* all general input method requirements described in the guide to
@@ -831,6 +838,7 @@ public final class VirtualDeviceParams implements Parcelable {
* may interact with the virtual device, then there will effectively be no IME on this
* device's displays for that user.
*
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
* @see android.inputmethodservice.InputMethodService
* @attr ref android.R.styleable#InputMethod_isVirtualDeviceOnly
* @attr ref android.R.styleable#InputMethod_showInInputMethodPicker
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 34bea1a4df6f..cccfdb0938e5 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -2334,9 +2334,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
* Whether an app allows its playback audio to be captured by other apps.
*
* @return {@code true} if the app indicates that its audio can be captured by other apps.
- *
- * @hide
*/
+ @FlaggedApi(Flags.FLAG_AUDIO_PLAYBACK_CAPTURE_ALLOWANCE)
public boolean isAudioPlaybackCaptureAllowed() {
return (privateFlags & PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE) != 0;
}
diff --git a/core/java/android/content/pm/SharedLibraryInfo.java b/core/java/android/content/pm/SharedLibraryInfo.java
index d77b2f53fc5b..f7191e605fb8 100644
--- a/core/java/android/content/pm/SharedLibraryInfo.java
+++ b/core/java/android/content/pm/SharedLibraryInfo.java
@@ -209,6 +209,24 @@ public final class SharedLibraryInfo implements Parcelable {
}
/**
+ * @hide
+ * @param name
+ * @param versionMajor
+ */
+ public SharedLibraryInfo(String name, long versionMajor, int type) {
+ mPath = null;
+ mPackageName = null;
+ mName = name;
+ mVersion = versionMajor;
+ mType = type;
+ mDeclaringPackage = null;
+ mDependentPackages = null;
+ mDependencies = null;
+ mIsNative = false;
+ mOptionalDependentPackages = null;
+ }
+
+ /**
* Gets the type of this library.
*
* @return The library type.
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 300740e84c60..c7d7dc1eb0de 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -288,6 +288,15 @@ flag {
}
flag {
+ name: "audio_playback_capture_allowance"
+ is_exported: true
+ namespace: "package_manager_service"
+ description: "Feature flag to enable the feature to retrieve info about audio playback capture allowance at manifest level."
+ bug: "362425551"
+ is_fixed_read_only: true
+}
+
+flag {
name: "get_packages_from_launcher_apps"
namespace: "package_manager_service"
description: "Feature flag to provide the new methods within launcher apps class to get packages."
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 6be7cc4ddf6a..ff0a3ddc746e 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -47,13 +47,6 @@ flag {
}
flag {
- name: "start_user_before_scheduled_alarms"
- namespace: "multiuser"
- description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due"
- bug: "314907186"
-}
-
-flag {
name: "add_ui_for_sounds_from_background_users"
namespace: "multiuser"
description: "Allow foreground user to dismiss sounds that are coming from background users"
@@ -198,46 +191,6 @@ flag {
}
flag {
- name: "cache_profile_parent"
- namespace: "multiuser"
- description: "Cache getProfileParent to avoid unnecessary binder calls"
- bug: "350417399"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
- name: "cache_profile_ids"
- namespace: "multiuser"
- description: "Cache getProfileIds to avoid unnecessary binder calls"
- bug: "350421409"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
- name: "cache_profile_type"
- namespace: "multiuser"
- description: "Cache getProfileType to avoid unnecessary binder calls"
- bug: "350417403"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
- name: "cache_profiles"
- namespace: "multiuser"
- description: "Cache getProfiles to avoid unnecessary binder calls"
- bug: "350419395"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "fix_disabling_of_mu_toggle_when_restriction_applied"
namespace: "multiuser"
description: "When no_user_switch is set but no EnforcedAdmin is present, the toggle has to be disabled"
@@ -334,6 +287,17 @@ flag {
}
flag {
+ name: "invalidate_cache_on_users_changed_read_only"
+ namespace: "multiuser"
+ description: "Invalidate the cache when users are added or removed to improve caches."
+ bug: "372383485"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+ is_fixed_read_only: true
+}
+
+flag {
name: "caches_not_invalidated_at_start_read_only"
namespace: "multiuser"
description: "PIC need to be invalidated at start in order to work properly."
diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java
index 74ce62c7abff..19a13db15b05 100644
--- a/core/java/android/content/pm/parsing/ApkLite.java
+++ b/core/java/android/content/pm/parsing/ApkLite.java
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.content.pm.ArchivedPackageParcel;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.SharedLibraryInfo;
import android.content.pm.SigningDetails;
import android.content.pm.VerifierInfo;
@@ -149,6 +150,8 @@ public class ApkLite {
*/
private final @Nullable String mEmergencyInstaller;
+ private final @NonNull List<SharedLibraryInfo> mDeclaredLibraries;
+
/**
* Archival install info.
*/
@@ -165,7 +168,7 @@ public class ApkLite {
int minSdkVersion, int targetSdkVersion, int rollbackDataPolicy,
Set<String> requiredSplitTypes, Set<String> splitTypes,
boolean hasDeviceAdminReceiver, boolean isSdkLibrary, boolean updatableSystem,
- String emergencyInstaller) {
+ String emergencyInstaller, List<SharedLibraryInfo> declaredLibraries) {
mPath = path;
mPackageName = packageName;
mSplitName = splitName;
@@ -202,6 +205,7 @@ public class ApkLite {
mUpdatableSystem = updatableSystem;
mEmergencyInstaller = emergencyInstaller;
mArchivedPackage = null;
+ mDeclaredLibraries = declaredLibraries;
}
public ApkLite(String path, ArchivedPackageParcel archivedPackage) {
@@ -241,6 +245,7 @@ public class ApkLite {
mUpdatableSystem = true;
mEmergencyInstaller = null;
mArchivedPackage = archivedPackage;
+ mDeclaredLibraries = null;
}
/**
@@ -565,6 +570,11 @@ public class ApkLite {
return mEmergencyInstaller;
}
+ @DataClass.Generated.Member
+ public @NonNull List<SharedLibraryInfo> getDeclaredLibraries() {
+ return mDeclaredLibraries;
+ }
+
/**
* Archival install info.
*/
@@ -574,10 +584,10 @@ public class ApkLite {
}
@DataClass.Generated(
- time = 1706896661616L,
+ time = 1728333566322L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java",
- inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index ffb69c0a2821..1a7f628ae61c 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -24,6 +24,7 @@ import android.annotation.NonNull;
import android.app.admin.DeviceAdminReceiver;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.SharedLibraryInfo;
import android.content.pm.SigningDetails;
import android.content.pm.VerifierInfo;
import android.content.pm.parsing.result.ParseInput;
@@ -92,6 +93,8 @@ public class ApkLiteParseUtils {
private static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES;
private static final String TAG_PROCESSES = "processes";
private static final String TAG_PROCESS = "process";
+ private static final String TAG_STATIC_LIBRARY = "static-library";
+ private static final String TAG_LIBRARY = "library";
/**
* Parse only lightweight details about the package at the given location.
@@ -457,6 +460,7 @@ public class ApkLiteParseUtils {
boolean hasDeviceAdminReceiver = false;
boolean isSdkLibrary = false;
+ List<SharedLibraryInfo> declaredLibraries = new ArrayList<>();
// Only search the tree when the tag is the direct child of <manifest> tag
int type;
@@ -521,6 +525,51 @@ public class ApkLiteParseUtils {
break;
case TAG_SDK_LIBRARY:
isSdkLibrary = true;
+ // Mirrors ParsingPackageUtils#parseSdkLibrary until lite and full
+ // parsing are combined
+ String sdkLibName = parser.getAttributeValue(
+ ANDROID_RES_NAMESPACE, "name");
+ int sdkLibVersionMajor = parser.getAttributeIntValue(
+ ANDROID_RES_NAMESPACE, "versionMajor", -1);
+ if (sdkLibName == null || sdkLibVersionMajor < 0) {
+ return input.error(
+ PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
+ "Bad uses-sdk-library declaration name: " + sdkLibName
+ + " version: " + sdkLibVersionMajor);
+ }
+ declaredLibraries.add(new SharedLibraryInfo(
+ sdkLibName, sdkLibVersionMajor,
+ SharedLibraryInfo.TYPE_SDK_PACKAGE));
+ break;
+ case TAG_STATIC_LIBRARY:
+ // Mirrors ParsingPackageUtils#parseStaticLibrary until lite and full
+ // parsing are combined
+ String staticLibName = parser.getAttributeValue(
+ ANDROID_RES_NAMESPACE, "name");
+ int staticLibVersion = parser.getAttributeIntValue(
+ ANDROID_RES_NAMESPACE, "version", -1);
+ int staticLibVersionMajor = parser.getAttributeIntValue(
+ ANDROID_RES_NAMESPACE, "versionMajor", 0);
+ if (staticLibName == null || staticLibVersion < 0) {
+ return input.error("Bad static-library declaration name: "
+ + staticLibName + " version: " + staticLibVersion);
+ }
+ declaredLibraries.add(new SharedLibraryInfo(staticLibName,
+ PackageInfo.composeLongVersionCode(staticLibVersionMajor,
+ staticLibVersion), SharedLibraryInfo.TYPE_STATIC));
+ break;
+ case TAG_LIBRARY:
+ // Mirrors ParsingPackageUtils#parseLibrary until lite and full parsing
+ // are combined
+ String libName = parser.getAttributeValue(
+ ANDROID_RES_NAMESPACE, "name");
+ if (libName == null) {
+ return input.error("Bad library declaration name: null");
+ }
+ libName = libName.intern();
+ declaredLibraries.add(new SharedLibraryInfo(libName,
+ SharedLibraryInfo.VERSION_UNDEFINED,
+ SharedLibraryInfo.TYPE_DYNAMIC));
break;
case TAG_PROCESSES:
final int processesDepth = parser.getDepth();
@@ -645,7 +694,8 @@ public class ApkLiteParseUtils {
overlayIsStatic, overlayPriority, requiredSystemPropertyName,
requiredSystemPropertyValue, minSdkVersion, targetSdkVersion,
rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second,
- hasDeviceAdminReceiver, isSdkLibrary, updatableSystem, emergencyInstaller));
+ hasDeviceAdminReceiver, isSdkLibrary, updatableSystem, emergencyInstaller,
+ declaredLibraries));
}
private static boolean isDeviceAdminReceiver(
diff --git a/core/java/android/content/pm/parsing/PackageLite.java b/core/java/android/content/pm/parsing/PackageLite.java
index 116dd1fc9a42..9a2ee7fe4cc6 100644
--- a/core/java/android/content/pm/parsing/PackageLite.java
+++ b/core/java/android/content/pm/parsing/PackageLite.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.ArchivedPackageParcel;
import android.content.pm.PackageInfo;
+import android.content.pm.SharedLibraryInfo;
import android.content.pm.SigningDetails;
import android.content.pm.VerifierInfo;
@@ -114,6 +115,8 @@ public class PackageLite {
*/
private final boolean mIsSdkLibrary;
+ private final @NonNull List<SharedLibraryInfo> mDeclaredLibraries;
+
/**
* Archival install info.
*/
@@ -154,6 +157,7 @@ public class PackageLite {
mSplitApkPaths = splitApkPaths;
mSplitRevisionCodes = splitRevisionCodes;
mTargetSdk = targetSdk;
+ mDeclaredLibraries = baseApk.getDeclaredLibraries();
mArchivedPackage = baseApk.getArchivedPackage();
}
@@ -433,6 +437,11 @@ public class PackageLite {
return mIsSdkLibrary;
}
+ @DataClass.Generated.Member
+ public @NonNull List<SharedLibraryInfo> getDeclaredLibraries() {
+ return mDeclaredLibraries;
+ }
+
/**
* Archival install info.
*/
@@ -442,10 +451,10 @@ public class PackageLite {
}
@DataClass.Generated(
- time = 1694792176268L,
+ time = 1728333569917L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/parsing/PackageLite.java",
- inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index a69a37133192..acb48f328f1a 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -2270,7 +2270,17 @@ public abstract class CameraMetadata<TKey> {
* {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} are ignored. The
* application has control over the various
* android.flash.* fields.</p>
+ * <p>If the device supports manual flash strength control, i.e.,
+ * if {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+ * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, then
+ * the auto-exposure (AE) precapture metering sequence should be
+ * triggered for the configured flash mode and strength to avoid
+ * the image being incorrectly exposed at different
+ * {@link CaptureRequest#FLASH_STRENGTH_LEVEL android.flash.strengthLevel}.</p>
*
+ * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+ * @see CaptureRequest#FLASH_STRENGTH_LEVEL
+ * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
* @see CaptureRequest#SENSOR_EXPOSURE_TIME
* @see CaptureRequest#SENSOR_FRAME_DURATION
* @see CaptureRequest#SENSOR_SENSITIVITY
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 3f5ae9196577..a193ee10b6e6 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -1358,6 +1358,13 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* camera device auto-exposure routine for the overridden
* fields for a given capture will be available in its
* CaptureResult.</p>
+ * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON and if the device
+ * supports manual flash strength control, i.e.,
+ * if {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+ * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, then
+ * the auto-exposure (AE) precapture metering sequence should be
+ * triggered to avoid the image being incorrectly exposed at
+ * different {@link CaptureRequest#FLASH_STRENGTH_LEVEL android.flash.strengthLevel}.</p>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #CONTROL_AE_MODE_OFF OFF}</li>
@@ -1373,9 +1380,13 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* <p>This key is available on all devices.</p>
*
* @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
+ * @see CaptureRequest#CONTROL_AE_MODE
* @see CaptureRequest#CONTROL_MODE
* @see CameraCharacteristics#FLASH_INFO_AVAILABLE
* @see CaptureRequest#FLASH_MODE
+ * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+ * @see CaptureRequest#FLASH_STRENGTH_LEVEL
+ * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
* @see CaptureRequest#SENSOR_EXPOSURE_TIME
* @see CaptureRequest#SENSOR_FRAME_DURATION
* @see CaptureRequest#SENSOR_SENSITIVITY
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index a18a634918f9..e5ca46ab0a72 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -759,6 +759,13 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* camera device auto-exposure routine for the overridden
* fields for a given capture will be available in its
* CaptureResult.</p>
+ * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON and if the device
+ * supports manual flash strength control, i.e.,
+ * if {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+ * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1, then
+ * the auto-exposure (AE) precapture metering sequence should be
+ * triggered to avoid the image being incorrectly exposed at
+ * different {@link CaptureRequest#FLASH_STRENGTH_LEVEL android.flash.strengthLevel}.</p>
* <p><b>Possible values:</b></p>
* <ul>
* <li>{@link #CONTROL_AE_MODE_OFF OFF}</li>
@@ -774,9 +781,13 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* <p>This key is available on all devices.</p>
*
* @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
+ * @see CaptureRequest#CONTROL_AE_MODE
* @see CaptureRequest#CONTROL_MODE
* @see CameraCharacteristics#FLASH_INFO_AVAILABLE
* @see CaptureRequest#FLASH_MODE
+ * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+ * @see CaptureRequest#FLASH_STRENGTH_LEVEL
+ * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
* @see CaptureRequest#SENSOR_EXPOSURE_TIME
* @see CaptureRequest#SENSOR_FRAME_DURATION
* @see CaptureRequest#SENSOR_SENSITIVITY
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 6c1aa90c831b..75ffcc3a8863 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -461,6 +461,16 @@ public abstract class DisplayManagerInternal {
public abstract void stylusGestureStarted(long eventTime);
/**
+ * Called by {@link com.android.server.wm.ContentRecorder} to verify whether
+ * the display is allowed to mirror primary display's content.
+ * @param displayId the id of the display where we mirror to.
+ * @return true if the mirroring dialog is confirmed (display is enabled), or
+ * {@link com.android.server.display.ExternalDisplayPolicy#ENABLE_ON_CONNECT}
+ * system property is enabled.
+ */
+ public abstract boolean isDisplayReadyForMirroring(int displayId);
+
+ /**
* Describes the requested power state of the display.
*
* This object is intended to describe the general characteristics of the
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 177ee6f1540a..897ce4a7b022 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -24,6 +24,8 @@ import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
+import static com.android.hardware.input.Flags.mouseReverseVerticalScrolling;
+import static com.android.hardware.input.Flags.mouseSwapPrimaryButton;
import static com.android.hardware.input.Flags.touchpadTapDragging;
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.input.flags.Flags.enableInputFilterRustImpl;
@@ -363,6 +365,22 @@ public class InputSettings {
}
/**
+ * Returns true if the feature flag for mouse reverse vertical scrolling is enabled.
+ * @hide
+ */
+ public static boolean isMouseReverseVerticalScrollingFeatureFlagEnabled() {
+ return mouseReverseVerticalScrolling();
+ }
+
+ /**
+ * Returns true if the feature flag for mouse swap primary button is enabled.
+ * @hide
+ */
+ public static boolean isMouseSwapPrimaryButtonFeatureFlagEnabled() {
+ return mouseSwapPrimaryButton();
+ }
+
+ /**
* Returns true if the touchpad visualizer is allowed to appear.
*
* @param context The application context.
@@ -501,6 +519,86 @@ public class InputSettings {
}
/**
+ * Whether mouse vertical scrolling is enabled, this applies only to connected mice.
+ *
+ * @param context The application context.
+ * @return Whether the mouse will have its vertical scrolling reversed
+ * (scroll down to move up).
+ *
+ * @hide
+ */
+ public static boolean isMouseReverseVerticalScrollingEnabled(@NonNull Context context) {
+ if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) {
+ return false;
+ }
+
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, 0, UserHandle.USER_CURRENT)
+ != 0;
+ }
+
+ /**
+ * Sets whether the connected mouse will have its vertical scrolling reversed.
+ *
+ * @param context The application context.
+ * @param reverseScrolling Whether reverse scrolling is enabled.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setMouseReverseVerticalScrolling(@NonNull Context context,
+ boolean reverseScrolling) {
+ if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) {
+ return;
+ }
+
+ Settings.System.putIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, reverseScrolling ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
+ * Whether the primary mouse button is swapped on connected mice.
+ *
+ * @param context The application context.
+ * @return Whether mice will have their primary buttons swapped, so that left clicking will
+ * perform the secondary action (e.g. show menu) and right clicking will perform the primary
+ * action.
+ *
+ * @hide
+ */
+ public static boolean isMouseSwapPrimaryButtonEnabled(@NonNull Context context) {
+ if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) {
+ return false;
+ }
+
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, 0, UserHandle.USER_CURRENT)
+ != 0;
+ }
+
+ /**
+ * Sets whether mice will have their primary buttons swapped between left and right
+ * clicks.
+ *
+ * @param context The application context.
+ * @param swapPrimaryButton Whether swapping the primary button is enabled.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setMouseSwapPrimaryButton(@NonNull Context context,
+ boolean swapPrimaryButton) {
+ if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) {
+ return;
+ }
+
+ Settings.System.putIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, swapPrimaryButton ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
* Whether Accessibility bounce keys feature is enabled.
*
* <p>
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
index e8ef8cd11585..3b74d7f5253d 100644
--- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -163,7 +163,6 @@ public abstract class VirtualInputDeviceConfig {
return self();
}
-
/**
* Sets the product id of the device, uniquely identifying the device within the address
* space of a given vendor, identified by the device's vendor id.
@@ -179,6 +178,10 @@ public abstract class VirtualInputDeviceConfig {
*
* <p>The input device is restricted to the display with the given ID and may not send
* events to any other display.</p>
+ * <p>The corresponding display must be trusted or mirror display.</p>
+ *
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
*/
@NonNull
public T setAssociatedDisplayId(int displayId) {
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 7d3076d6611f..a1b75034442e 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -115,3 +115,10 @@ per-file OomKillRecord.java = file:/MEMORY_OWNERS
# MessageQueue
per-file MessageQueue.java = mfasheh@google.com, shayba@google.com
per-file Message.java = mfasheh@google.com, shayba@google.com
+
+# Stats
+per-file IStatsBootstrapAtomService.aidl = file:/services/core/java/com/android/server/stats/OWNERS
+per-file StatsBootstrapAtom.aidl = file:/services/core/java/com/android/server/stats/OWNERS
+per-file StatsBootstrapAtomValue.aidl = file:/services/core/java/com/android/server/stats/OWNERS
+per-file StatsBootstrapAtomService.java = file:/services/core/java/com/android/server/stats/OWNERS
+per-file StatsServiceManager.java = file:/services/core/java/com/android/server/stats/OWNERS
diff --git a/core/java/android/os/StatsBootstrapAtomValue.aidl b/core/java/android/os/StatsBootstrapAtomValue.aidl
index a90dfa404ee9..b59bc062648f 100644
--- a/core/java/android/os/StatsBootstrapAtomValue.aidl
+++ b/core/java/android/os/StatsBootstrapAtomValue.aidl
@@ -26,4 +26,5 @@ union StatsBootstrapAtomValue {
float floatValue;
String stringValue;
byte[] bytesValue;
+ String[] stringArrayValue;
} \ No newline at end of file
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 3ae951170759..f1964e7bc024 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -6368,8 +6368,12 @@ public class UserManager {
Settings.Global.DEVICE_DEMO_MODE, 0) > 0;
}
- /** @hide */
- public static final void invalidateUserSerialNumberCache() {
+
+ /**
+ * This method is used to invalidate caches, when user was added or removed.
+ * @hide
+ */
+ public static final void invalidateCacheOnUserListChange() {
UserManagerCache.invalidateUserSerialNumber();
}
@@ -6382,7 +6386,7 @@ public class UserManager {
* @hide
*/
@UnsupportedAppUsage
- @CachedProperty(modsFlagOnOrNone = {})
+ @CachedProperty(modsFlagOnOrNone = {}, api = "user_manager_users")
public int getUserSerialNumber(@UserIdInt int userId) {
// Read only flag should is to fix early access to this API
// cacheUserSerialNumber to be removed after the
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index a1bfe39c0fc4..81987907452f 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -232,3 +232,19 @@ flag {
bug: "361329788"
is_exported: true
}
+
+flag {
+ name: "enable_angle_allow_list"
+ namespace: "gpu"
+ description: "Whether to read from angle allowlist to determine if app should use ANGLE"
+ is_fixed_read_only: true
+ bug: "370845648"
+}
+
+flag {
+ name: "api_for_backported_fixes"
+ namespace: "media_reliability"
+ description: "Public API app developers use to check if a known issue is fixed on a device."
+ bug: "308461809"
+ is_exported: true
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index feeb339c1200..1d8fcec8cf31 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -255,3 +255,14 @@ flag {
description: "This fixed read-only flag is used to enable replacing permission BODY_SENSORS (and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE (and READ_HEALTH_DATA_IN_BACKGROUND)"
bug: "364638912"
}
+
+flag {
+ name: "delay_uid_state_changes_from_capability_updates"
+ is_fixed_read_only: true
+ namespace: "permissions"
+ description: "If proc state is decreasing over the restriction threshold and capability is changed, delay if no new capabilities are added"
+ bug: "347891382"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1a15d09c0a50..594005c3ebd6 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2351,6 +2351,11 @@ public final class Settings {
/**
* Activity Action: Show the permission screen for allowing apps to post promoted notifications.
+ * Properly formatted priority notifications are elevated in appearance. For example they may be
+ * able to use colors, have richer progress bars, show as chips in the status bar, and/or
+ * permanently appear on always-on-displays. This functionality is intended to be reserved for
+ * user initiated ongoing activities like navigation, phone calls, and ride sharing.
+ *
* <p>
* Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
* <p>
@@ -6205,6 +6210,25 @@ public final class Settings {
public static final String TOUCHPAD_RIGHT_CLICK_ZONE = "touchpad_right_click_zone";
/**
+ * Whether to enable reversed vertical scrolling for connected mice.
+ *
+ * When enabled, scrolling down on the mouse wheel will move the screen up and vice versa.
+ * @hide
+ */
+ public static final String MOUSE_REVERSE_VERTICAL_SCROLLING =
+ "mouse_reverse_vertical_scrolling";
+
+ /**
+ * Whether to enable swapping the primary button for connected mice.
+ *
+ * When enabled, right clicking will be the primary button and left clicking will be the
+ * secondary button (e.g. show menu).
+ * @hide
+ */
+ public static final String MOUSE_SWAP_PRIMARY_BUTTON =
+ "mouse_swap_primary_button";
+
+ /**
* Pointer fill style, specified by
* {@link android.view.PointerIcon.PointerIconVectorStyleFill} constants.
*
@@ -6442,6 +6466,8 @@ public final class Settings {
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR);
PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE);
+ PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING);
+ PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON);
}
/**
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index f7745d14188e..83b4971c8621 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.view.flags.Flags.FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER;
import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_SUBLAYER;
@@ -770,6 +771,36 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
}
/**
+ * Controls the composition order of the SurfaceView. A non-negative composition order
+ * value indicates that the SurfaceView is composited on top of the parent window, while
+ * a negative composition order indicates that the SurfaceView is behind the parent
+ * window. A SurfaceView with a higher value appears above its peers with lower values.
+ * For SurfaceViews with the same composition order value, their relative order is
+ * undefined.
+ *
+ * @param compositionOrder the composition order of the surface view.
+ */
+ @FlaggedApi(FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER)
+ public void setCompositionOrder(int compositionOrder) {
+ mRequestedSubLayer = compositionOrder;
+ if (mSubLayer != mRequestedSubLayer) {
+ updateSurface();
+ }
+ }
+
+ /**
+ * Returns the composition order of the SurfaceView.
+ *
+ * @return composition order of the SurfaceView.
+ *
+ * @see #setCompositionOrder(int)
+ */
+ @FlaggedApi(FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER)
+ public int getCompositionOrder() {
+ return mRequestedSubLayer;
+ }
+
+ /**
* Control whether the surface view's surface is placed on top of another
* regular surface view in the window (but still behind the window itself).
* This is typically used to place overlays on top of an underlying media
@@ -1257,7 +1288,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
final Transaction surfaceUpdateTransaction = new Transaction();
if (creating) {
updateOpaqueFlag();
- final String name = "SurfaceView[" + viewRoot.getTitle().toString() + "]";
+ final String name = Integer.toHexString(System.identityHashCode(this))
+ + " SurfaceView[" + viewRoot.getTitle().toString() + "]";
createBlastSurfaceControls(viewRoot, name, surfaceUpdateTransaction);
} else if (mSurfaceControl == null) {
return;
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java
index a43906f30ff7..dfac244fcc0d 100644
--- a/core/java/android/view/accessibility/AccessibilityEvent.java
+++ b/core/java/android/view/accessibility/AccessibilityEvent.java
@@ -16,6 +16,7 @@
package android.view.accessibility;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
@@ -784,6 +785,19 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
*/
public static final int CONTENT_CHANGE_TYPE_ENABLED = 1 << 12;
+ /**
+ * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
+ * The source node changed its checked state, which is returned by
+ * {@link AccessibilityNodeInfo#getChecked()}.
+ * The view changing its checked state should call
+ * {@link AccessibilityNodeInfo#setChecked(int)} and then send this event.
+ *
+ * @see AccessibilityNodeInfo#getChecked()
+ * @see AccessibilityNodeInfo#setChecked(int)
+ */
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ public static final int CONTENT_CHANGE_TYPE_CHECKED = 1 << 13;
+
// Speech state change types.
/** Change type for {@link #TYPE_SPEECH_STATE_CHANGE} event: another service is speaking. */
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index fe6aafbd7e16..c5ca059d1cea 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -860,6 +860,37 @@ public class AccessibilityNodeInfo implements Parcelable {
public static final String EXTRA_DATA_REQUESTED_KEY =
"android.view.accessibility.AccessibilityNodeInfo.extra_data_requested";
+ // Tri-state checked states.
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CHECKED_STATE" }, value = {
+ CHECKED_STATE_FALSE,
+ CHECKED_STATE_TRUE,
+ CHECKED_STATE_PARTIAL
+ })
+ public @interface CheckedState {}
+
+ /**
+ * This node is not checked.
+ */
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ public static final int CHECKED_STATE_FALSE = 0;
+
+ /**
+ * This node is checked.
+ */
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ public static final int CHECKED_STATE_TRUE = 1;
+
+ /**
+ * This node is partially checked. For example,
+ * when a checkbox owns a number of sub-options and they have
+ * different states, then the main checkbox is in a partially-checked state.
+ */
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ public static final int CHECKED_STATE_PARTIAL = 2;
+
// Boolean attributes.
private static final int BOOLEAN_PROPERTY_CHECKABLE = 1 /* << 0 */;
@@ -1038,6 +1069,10 @@ public class AccessibilityNodeInfo implements Parcelable {
private IBinder mLeashedParent;
private long mLeashedParentNodeId = UNDEFINED_NODE_ID;
+ // TODO(b/369951517) Initialize mChecked explicitly with
+ // the CHECKED_FALSE state when flagging is removed.
+ private int mChecked;
+
/**
* Creates a new {@link AccessibilityNodeInfo}.
*/
@@ -2319,28 +2354,100 @@ public class AccessibilityNodeInfo implements Parcelable {
}
/**
- * Gets whether this node is checked.
+ * Gets whether this node is checked. This is only meaningful
+ * when {@link #isCheckable()} returns {@code true}.
+ *
+ * @deprecated Use {@link #getChecked()} instead.
*
* @return True if the node is checked.
*/
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ @Deprecated
public boolean isChecked() {
return getBooleanProperty(BOOLEAN_PROPERTY_CHECKED);
}
/**
- * Sets whether this node is checked.
+ * Sets whether this node is checked. This is only meaningful
+ * when {@link #isCheckable()} returns {@code true}.
* <p>
* <strong>Note:</strong> Cannot be called from an
* {@link android.accessibilityservice.AccessibilityService}.
* This class is made immutable before being delivered to an AccessibilityService.
* </p>
*
+ * @deprecated Use {@link #setChecked(int)} instead.
+ *
* @param checked True if the node is checked.
*
* @throws IllegalStateException If called from an AccessibilityService.
*/
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ @Deprecated
public void setChecked(boolean checked) {
setBooleanProperty(BOOLEAN_PROPERTY_CHECKED, checked);
+ if (Flags.triStateChecked()) {
+ mChecked = checked ? CHECKED_STATE_TRUE : CHECKED_STATE_FALSE;
+ }
+ }
+
+ /**
+ * Gets the checked state of this node. This is only meaningful
+ * when {@link #isCheckable()} returns {@code true}.
+ *
+ * @see #setCheckable(boolean)
+ * @see #isCheckable()
+ * @see #setChecked(int)
+ *
+ * @return The checked state, one of:
+ * <ul>
+ * <li>{@link #CHECKED_STATE_FALSE}
+ * <li>{@link #CHECKED_STATE_TRUE}
+ * <li>{@link #CHECKED_STATE_PARTIAL}
+ * </ul>
+ */
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ public @CheckedState int getChecked() {
+ return mChecked;
+ }
+
+ /**
+ * Sets the checked state of this node. This is only meaningful
+ * when {@link #isCheckable()} returns {@code true}.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @see #setCheckable(boolean)
+ * @see #isCheckable()
+ * @see #getChecked()
+ *
+ * @param checked The checked state. One of
+ * <ul>
+ * <li>{@link #CHECKED_STATE_FALSE}
+ * <li>{@link #CHECKED_STATE_TRUE}
+ * <li>{@link #CHECKED_STATE_PARTIAL}
+ * </ul>
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ * @throws IllegalArgumentException if checked is not one of {@link #CHECKED_STATE_FALSE},
+ * {@link #CHECKED_STATE_TRUE}, or {@link #CHECKED_STATE_PARTIAL}.
+ */
+ @FlaggedApi(Flags.FLAG_TRI_STATE_CHECKED)
+ public void setChecked(@CheckedState int checked) {
+ enforceNotSealed();
+ switch (checked) {
+ case CHECKED_STATE_FALSE:
+ case CHECKED_STATE_TRUE:
+ case CHECKED_STATE_PARTIAL:
+ mChecked = checked;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown checked argument: " + checked);
+ }
+ setBooleanProperty(BOOLEAN_PROPERTY_CHECKED, checked == CHECKED_STATE_TRUE);
}
/**
@@ -4515,6 +4622,10 @@ public class AccessibilityNodeInfo implements Parcelable {
if (mLeashedParentNodeId != DEFAULT.mLeashedParentNodeId) {
nonDefaultFields |= bitAt(fieldIndex);
}
+ fieldIndex++;
+ if (mChecked != DEFAULT.mChecked) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
int totalFields = fieldIndex;
parcel.writeLong(nonDefaultFields);
@@ -4683,6 +4794,9 @@ public class AccessibilityNodeInfo implements Parcelable {
if (isBitSet(nonDefaultFields, fieldIndex++)) {
parcel.writeLong(mLeashedParentNodeId);
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ parcel.writeInt(mChecked);
+ }
if (DEBUG) {
fieldIndex--;
@@ -4771,6 +4885,7 @@ public class AccessibilityNodeInfo implements Parcelable {
mLeashedChild = other.mLeashedChild;
mLeashedParent = other.mLeashedParent;
mLeashedParentNodeId = other.mLeashedParentNodeId;
+ mChecked = other.mChecked;
}
private void initCopyInfos(AccessibilityNodeInfo other) {
@@ -4960,6 +5075,9 @@ public class AccessibilityNodeInfo implements Parcelable {
if (isBitSet(nonDefaultFields, fieldIndex++)) {
mLeashedParentNodeId = parcel.readLong();
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) {
+ mChecked = parcel.readInt();
+ }
mSealed = sealed;
}
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 69cbb9b2bfb8..8ffae845de1f 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -219,6 +219,13 @@ flag {
}
flag {
+ name: "tri_state_checked"
+ namespace: "accessibility"
+ description: "Feature flag for adding tri-state checked api"
+ bug: "333784774"
+}
+
+flag {
name: "warning_use_default_dialog_type"
namespace: "accessibility"
description: "Uses the default type for the A11yService warning dialog, instead of SYSTEM_ALERT_DIALOG"
@@ -226,4 +233,4 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-}
+ }
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 5115b132af93..1cf26ab64c09 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -109,3 +109,11 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "surface_view_set_composition_order"
+ namespace: "window_surfaces"
+ description: "Add a SurfaceView composition order control API."
+ bug: "341021569"
+ is_fixed_read_only: true
+} \ No newline at end of file
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index 7366b9a443c3..ab5969bb381c 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -16,10 +16,12 @@
package android.webkit;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -151,6 +153,24 @@ public abstract class WebSettings {
public static final long ENABLE_SIMPLIFIED_DARK_MODE = 214741472L;
/**
+ * Enable User-Agent Reduction for webview.
+ * The OS, CPU, and Build information in the default User-Agent will be
+ * reduced to the static "Linux; Android 10; K" string.
+ * Minor/build/patch version information in the default User-Agent is
+ * reduced to "0.0.0". The rest of the default User-Agent remains unchanged.
+ *
+ * See https://developers.google.com/privacy-sandbox/protections/user-agent
+ * for details related to User-Agent Reduction.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @FlaggedApi(android.webkit.Flags.FLAG_USER_AGENT_REDUCTION)
+ @SystemApi
+ public static final long ENABLE_USER_AGENT_REDUCTION = 371034303L;
+
+ /**
* Default cache usage mode. If the navigation type doesn't impose any
* specific behavior, use cached resources when they are available
* and not expired, otherwise load resources from the network.
diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig
index b21a490cc506..d1013a92476f 100644
--- a/core/java/android/webkit/flags.aconfig
+++ b/core/java/android/webkit/flags.aconfig
@@ -18,3 +18,11 @@ flag {
bug: "310653407"
is_fixed_read_only: true
}
+
+flag {
+ name: "user_agent_reduction"
+ is_exported: true
+ namespace: "webview"
+ description: "New feature reduce user-agent for webview"
+ bug: "371034303"
+}
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 6fb82af2b292..8e35843e2193 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -65,7 +65,9 @@ public enum DesktopModeFlags {
ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(
Flags::enableDesktopWindowingTaskbarRunningApps, true),
ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false),
- ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false);
+ ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false),
+ ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
+ Flags::enableWindowingTransitionHandlersObservers, false);
private static final String TAG = "DesktopModeFlagsUtil";
// Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/OWNERS b/core/java/android/window/OWNERS
index 2c61df96eb03..77c99b98cf4a 100644
--- a/core/java/android/window/OWNERS
+++ b/core/java/android/window/OWNERS
@@ -1,3 +1,5 @@
set noparent
include /services/core/java/com/android/server/wm/OWNERS
+
+per-file DesktopModeFlags.java = file:/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 31bb3a610376..155494fb3b25 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -228,7 +228,7 @@ flag {
name: "enable_desktop_windowing_app_handle_education"
namespace: "lse_desktop_experience"
description: "Enables desktop windowing app handle education"
- bug: "348208342"
+ bug: "316006079"
}
flag {
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index 39aadfb24b0c..8faaf9584e54 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -53,19 +53,18 @@ import java.util.Map;
* @hide
*/
public class AconfigFlags {
+ private static final boolean DEBUG = false;
private static final String LOG_TAG = "AconfigFlags";
-
- public enum Permission {
- READ_WRITE,
- READ_ONLY
- }
+ private static final String OVERRIDE_PREFIX = "device_config_overrides/";
+ private static final String STAGED_PREFIX = "staged/";
private final ArrayMap<String, Boolean> mFlagValues = new ArrayMap<>();
- private final ArrayMap<String, Permission> mFlagPermissions = new ArrayMap<>();
public AconfigFlags() {
if (!Flags.manifestFlagging()) {
- Slog.v(LOG_TAG, "Feature disabled, skipped all loading");
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "Feature disabled, skipped all loading");
+ }
return;
}
final var defaultFlagProtoFiles =
@@ -130,19 +129,17 @@ public class AconfigFlags {
if (!"false".equalsIgnoreCase(value) && !"true".equalsIgnoreCase(value)) {
continue;
}
- final var overridePrefix = "device_config_overrides/";
- final var stagedPrefix = "staged/";
String separator = "/";
String prefix = "default";
int priority = 0;
- if (name.startsWith(overridePrefix)) {
- prefix = overridePrefix;
- name = name.substring(overridePrefix.length());
+ if (name.startsWith(OVERRIDE_PREFIX)) {
+ prefix = OVERRIDE_PREFIX;
+ name = name.substring(OVERRIDE_PREFIX.length());
separator = ":";
priority = 20;
- } else if (name.startsWith(stagedPrefix)) {
- prefix = stagedPrefix;
- name = name.substring(stagedPrefix.length());
+ } else if (name.startsWith(STAGED_PREFIX)) {
+ prefix = STAGED_PREFIX;
+ name = name.substring(STAGED_PREFIX.length());
separator = "*";
priority = 10;
}
@@ -155,12 +152,19 @@ public class AconfigFlags {
if (!mFlagValues.containsKey(flagPackageAndName)) {
continue;
}
- Slog.d(LOG_TAG, "Found " + prefix
- + " Aconfig flag value for " + flagPackageAndName + " = " + value);
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Found " + prefix
+ + " Aconfig flag value in settings for " + flagPackageAndName
+ + " = " + value);
+ }
final Integer currentPriority = flagPriority.get(flagPackageAndName);
if (currentPriority != null && currentPriority >= priority) {
- Slog.i(LOG_TAG, "Skipping " + prefix + " flag " + flagPackageAndName
- + " because of the existing one with priority " + currentPriority);
+ if (DEBUG) {
+ Slog.d(LOG_TAG, "Skipping " + prefix + " flag "
+ + flagPackageAndName
+ + " in settings because of existing one with priority "
+ + currentPriority);
+ }
continue;
}
flagPriority.put(flagPackageAndName, priority);
@@ -185,15 +189,7 @@ public class AconfigFlags {
for (parsed_flag flag : parsedFlags.parsedFlag) {
String flagPackageAndName = flag.package_ + "." + flag.name;
boolean flagValue = (flag.state == Aconfig.ENABLED);
- Slog.v(LOG_TAG, "Read Aconfig default flag value "
- + flagPackageAndName + " = " + flagValue);
mFlagValues.put(flagPackageAndName, flagValue);
-
- Permission permission = flag.permission == Aconfig.READ_ONLY
- ? Permission.READ_ONLY
- : Permission.READ_WRITE;
-
- mFlagPermissions.put(flagPackageAndName, permission);
}
}
@@ -203,24 +199,15 @@ public class AconfigFlags {
* @return the current value of the given Aconfig flag, or null if there is no such flag
*/
@Nullable
- public Boolean getFlagValue(@NonNull String flagPackageAndName) {
+ private Boolean getFlagValue(@NonNull String flagPackageAndName) {
Boolean value = mFlagValues.get(flagPackageAndName);
- Slog.d(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
+ if (DEBUG) {
+ Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value);
+ }
return value;
}
/**
- * Get the flag permission, or null if the flag doesn't exist.
- * @param flagPackageAndName Full flag name formatted as 'package.flag'
- * @return the current permission of the given Aconfig flag, or null if there is no such flag
- */
- @Nullable
- public Permission getFlagPermission(@NonNull String flagPackageAndName) {
- Permission permission = mFlagPermissions.get(flagPackageAndName);
- return permission;
- }
-
- /**
* Check if the element in {@code parser} should be skipped because of the feature flag.
* @param parser XML parser object currently parsing an element
* @return true if the element is disabled because of its feature flag
@@ -247,7 +234,7 @@ public class AconfigFlags {
}
// Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated)
if (flagValue == negated) {
- Slog.v(LOG_TAG, "Skipping element " + parser.getName()
+ Slog.i(LOG_TAG, "Skipping element " + parser.getName()
+ " behind feature flag " + featureFlag + " = " + flagValue);
return true;
}
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
new file mode 100644
index 000000000000..12e1dd9b6cc1
--- /dev/null
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.app.Notification.ProgressStyle;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+import android.widget.RemoteViews;
+
+import androidx.annotation.ColorInt;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.NotificationProgressDrawable.Part;
+import com.android.internal.widget.NotificationProgressDrawable.Point;
+import com.android.internal.widget.NotificationProgressDrawable.Segment;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * NotificationProgressBar extends the capabilities of ProgressBar by adding functionalities to
+ * represent Notification ProgressStyle progress, such as for ridesharing and navigation.
+ */
+@RemoteViews.RemoteView
+public class NotificationProgressBar extends ProgressBar {
+ public NotificationProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public NotificationProgressBar(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.progressBarStyle);
+ }
+
+ public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NotificationProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Processes the ProgressStyle data and convert to list of {@code
+ * NotificationProgressDrawable.Part}.
+ */
+ @VisibleForTesting
+ public static List<Part> processAndConvertToDrawableParts(
+ List<ProgressStyle.Segment> segments,
+ List<ProgressStyle.Point> points,
+ int progress,
+ boolean isStyledByProgress
+ ) {
+ if (segments.isEmpty()) {
+ throw new IllegalArgumentException("List of segments shouldn't be empty");
+ }
+
+ for (ProgressStyle.Segment segment : segments) {
+ final int length = segment.getLength();
+ if (length <= 0) {
+ throw new IllegalArgumentException("Invalid segment length : " + length);
+ }
+ }
+
+ final int progressMax = segments.stream().mapToInt(ProgressStyle.Segment::getLength).sum();
+
+ if (progress < 0 || progress > progressMax) {
+ throw new IllegalArgumentException("Invalid progress : " + progress);
+ }
+ for (ProgressStyle.Point point : points) {
+ final int pos = point.getPosition();
+ if (pos <= 0 || pos >= progressMax) {
+ throw new IllegalArgumentException("Invalid Point position : " + pos);
+ }
+ }
+
+ final Map<Integer, ProgressStyle.Segment> startToSegmentMap = generateStartToSegmentMap(
+ segments);
+ final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap(
+ points);
+ final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap,
+ positionToPointMap, progress, isStyledByProgress);
+
+ final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap =
+ splitSegmentsByPointsAndProgress(
+ startToSegmentMap, sortedPos, progressMax);
+
+ return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos,
+ progress, progressMax,
+ isStyledByProgress);
+ }
+
+
+ // Any segment with a point on it gets split by the point.
+ // If isStyledByProgress is true, also split the segment with the progress value in its range.
+ private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress(
+ Map<Integer, ProgressStyle.Segment> startToSegmentMap,
+ SortedSet<Integer> sortedPos,
+ int progressMax) {
+ int prevSegStart = 0;
+ for (Integer pos : sortedPos) {
+ if (pos == 0 || pos == progressMax) continue;
+ if (startToSegmentMap.containsKey(pos)) {
+ prevSegStart = pos;
+ continue;
+ }
+
+ final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart);
+ final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment(
+ pos - prevSegStart).setColor(
+ prevSeg.getColor());
+ final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment(
+ prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor());
+
+ startToSegmentMap.put(prevSegStart, leftSeg);
+ startToSegmentMap.put(pos, rightSeg);
+
+ prevSegStart = pos;
+ }
+
+ return startToSegmentMap;
+ }
+
+ private static List<Part> convertToDrawableParts(
+ Map<Integer, ProgressStyle.Segment> startToSegmentMap,
+ Map<Integer, ProgressStyle.Point> positionToPointMap,
+ SortedSet<Integer> sortedPos,
+ int progress,
+ int progressMax,
+ boolean isStyledByProgress
+ ) {
+ List<Part> parts = new ArrayList<>();
+ boolean styleRemainingParts = false;
+ for (Integer pos : sortedPos) {
+ if (positionToPointMap.containsKey(pos)) {
+ final ProgressStyle.Point point = positionToPointMap.get(pos);
+ final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts);
+ parts.add(new Point(null, color, styleRemainingParts));
+ }
+ // We want the Point at the current progress to be filled (not faded), but a Segment
+ // starting at this progress to be faded.
+ if (isStyledByProgress && !styleRemainingParts && pos == progress) {
+ styleRemainingParts = true;
+ }
+ if (startToSegmentMap.containsKey(pos)) {
+ final ProgressStyle.Segment seg = startToSegmentMap.get(pos);
+ final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts);
+ parts.add(new Segment(
+ (float) seg.getLength() / progressMax, color, styleRemainingParts));
+ }
+ }
+
+ return parts;
+ }
+
+ @ColorInt
+ private static int maybeGetFadedColor(@ColorInt int color, boolean fade) {
+ if (!fade) return color;
+
+ return NotificationProgressDrawable.getFadedColor(color);
+ }
+
+ private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap(
+ List<ProgressStyle.Segment> segments) {
+ final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>();
+
+ int currentStart = 0; // Initial start position is 0
+
+ for (ProgressStyle.Segment segment : segments) {
+ // Use the current start position as the key, and the segment as the value
+ startToSegmentMap.put(currentStart, segment);
+
+ // Update the start position for the next segment
+ currentStart += segment.getLength();
+ }
+
+ return startToSegmentMap;
+ }
+
+ private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap(
+ List<ProgressStyle.Point> points) {
+ final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>();
+
+ for (ProgressStyle.Point point : points) {
+ positionToPointMap.put(point.getPosition(), point);
+ }
+
+ return positionToPointMap;
+ }
+
+ private static SortedSet<Integer> generateSortedPositionSet(
+ Map<Integer, ProgressStyle.Segment> startToSegmentMap,
+ Map<Integer, ProgressStyle.Point> positionToPointMap, int progress,
+ boolean isStyledByProgress) {
+ final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet());
+ sortedPos.addAll(positionToPointMap.keySet());
+ if (isStyledByProgress) {
+ sortedPos.add(progress);
+ }
+
+ return sortedPos;
+ }
+}
diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
index 4d88546a1bd8..89ef8759a169 100644
--- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java
+++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java
@@ -45,6 +45,7 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Objects;
/**
* This is used by NotificationProgressBar for displaying a custom background. It composes of
@@ -126,7 +127,7 @@ public final class NotificationProgressDrawable extends Drawable {
* @see #setStroke(int, int, float, float)
*/
public void setStrokeDefaultColor(@ColorInt int color) {
- mState.mStrokeColor = color;
+ mState.setStrokeColor(color);
}
/**
@@ -138,7 +139,7 @@ public final class NotificationProgressDrawable extends Drawable {
* @see #mutate()
*/
public void setPointRectDefaultColor(@ColorInt int color) {
- mState.mPointRectColor = color;
+ mState.setPointRectColor(color);
}
private void setStrokeInternal(int width, float dashWidth, float dashGap) {
@@ -194,7 +195,7 @@ public final class NotificationProgressDrawable extends Drawable {
mStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
: mState.mStrokeColor);
mDashedStrokePaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor
- : mState.mStrokeColor);
+ : mState.mFadedStrokeColor);
// Leave space for the rounded line cap which extends beyond start/end.
final float capWidth = mStrokePaint.getStrokeWidth() / 2F;
@@ -220,7 +221,8 @@ public final class NotificationProgressDrawable extends Drawable {
mPointRectF.inset(inset, inset);
mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor
- : mState.mPointRectColor);
+ : (point.mFaded ? mState.mFadedPointRectColor
+ : mState.mPointRectColor));
canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint);
}
@@ -424,8 +426,9 @@ public final class NotificationProgressDrawable extends Drawable {
state.mPointRectCornerRadius = a.getDimension(
R.styleable.NotificationProgressDrawablePoints_cornerRadius,
state.mPointRectCornerRadius);
- state.mPointRectColor = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
+ final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color,
state.mPointRectColor);
+ setPointRectDefaultColor(color);
}
static int resolveDensity(@Nullable Resources r, int parentDensity) {
@@ -478,7 +481,6 @@ public final class NotificationProgressDrawable extends Drawable {
* {@link Point} with zero length.
*/
public interface Part {
-
}
/**
@@ -521,6 +523,24 @@ public final class NotificationProgressDrawable extends Drawable {
return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", dashed="
+ this.mDashed + ')';
}
+
+ // Needed for unit tests
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) return true;
+
+ if (other == null || getClass() != other.getClass()) return false;
+
+ Segment that = (Segment) other;
+ if (Float.compare(this.mFraction, that.mFraction) != 0) return false;
+ if (this.mColor != that.mColor) return false;
+ return this.mDashed == that.mDashed;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mFraction, mColor, mDashed);
+ }
}
/**
@@ -532,14 +552,21 @@ public final class NotificationProgressDrawable extends Drawable {
@Nullable
private final Drawable mIcon;
@ColorInt private final int mColor;
+ private final boolean mFaded;
public Point(@Nullable Drawable icon) {
- this(icon, Color.TRANSPARENT);
+ this(icon, Color.TRANSPARENT, false);
}
public Point(@Nullable Drawable icon, @ColorInt int color) {
+ this(icon, color, false);
+
+ }
+
+ public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) {
mIcon = icon;
mColor = color;
+ mFaded = faded;
}
@Nullable
@@ -547,9 +574,37 @@ public final class NotificationProgressDrawable extends Drawable {
return this.mIcon;
}
+ public int getColor() {
+ return this.mColor;
+ }
+
+ public boolean getFaded() {
+ return this.mFaded;
+ }
+
@Override
public String toString() {
- return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ')';
+ return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded
+ + ")";
+ }
+
+ // Needed for unit tests.
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) return true;
+
+ if (other == null || getClass() != other.getClass()) return false;
+
+ Point that = (Point) other;
+
+ if (!Objects.equals(this.mIcon, that.mIcon)) return false;
+ if (this.mColor != that.mColor) return false;
+ return this.mFaded == that.mFaded;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIcon, mColor, mFaded);
}
}
@@ -576,12 +631,14 @@ public final class NotificationProgressDrawable extends Drawable {
float mSegPointGap = 0.0f;
int mStrokeWidth = 0;
int mStrokeColor;
+ int mFadedStrokeColor;
float mStrokeDashWidth = 0.0f;
float mStrokeDashGap = 0.0f;
float mPointRadius;
float mPointRectInset;
float mPointRectCornerRadius;
int mPointRectColor;
+ int mFadedPointRectColor;
int[] mThemeAttrs;
int[] mThemeAttrsSegments;
@@ -595,6 +652,7 @@ public final class NotificationProgressDrawable extends Drawable {
State(@NonNull State orig, @Nullable Resources res) {
mChangingConfigurations = orig.mChangingConfigurations;
mStrokeColor = orig.mStrokeColor;
+ mFadedStrokeColor = orig.mFadedStrokeColor;
mStrokeWidth = orig.mStrokeWidth;
mStrokeDashWidth = orig.mStrokeDashWidth;
mStrokeDashGap = orig.mStrokeDashGap;
@@ -602,6 +660,7 @@ public final class NotificationProgressDrawable extends Drawable {
mPointRectInset = orig.mPointRectInset;
mPointRectCornerRadius = orig.mPointRectCornerRadius;
mPointRectColor = orig.mPointRectColor;
+ mFadedPointRectColor = orig.mFadedPointRectColor;
mThemeAttrs = orig.mThemeAttrs;
mThemeAttrsSegments = orig.mThemeAttrsSegments;
@@ -683,10 +742,30 @@ public final class NotificationProgressDrawable extends Drawable {
public void setStroke(int width, int color, float dashWidth, float dashGap) {
mStrokeWidth = width;
- mStrokeColor = color;
mStrokeDashWidth = dashWidth;
mStrokeDashGap = dashGap;
+
+ setStrokeColor(color);
}
+
+ public void setStrokeColor(int color) {
+ mStrokeColor = color;
+ mFadedStrokeColor = getFadedColor(color);
+ }
+
+ public void setPointRectColor(int color) {
+ mPointRectColor = color;
+ mFadedPointRectColor = getFadedColor(color);
+ }
+ }
+
+ /**
+ * Get a color with an opacity that's 50% of the input color.
+ */
+ @ColorInt
+ static int getFadedColor(@ColorInt int color) {
+ return Color.argb(Color.alpha(color) / 2, Color.red(color), Color.green(color),
+ Color.blue(color));
}
@Override
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index e65b4b65945f..46855c64d6ae 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -16,14 +16,19 @@
package com.android.internal.widget;
+import static java.lang.Float.NaN;
+
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Path;
+import android.graphics.PorterDuff;
import android.graphics.RectF;
import android.graphics.Region;
import android.hardware.input.InputManager;
@@ -40,6 +45,7 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.RoundedCorner;
+import android.view.Surface;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
@@ -65,18 +71,21 @@ public class PointerLocationView extends View implements InputDeviceListener,
private static final PointerState EMPTY_POINTER_STATE = new PointerState();
public static class PointerState {
- // Trace of previous points.
- private float[] mTraceX = new float[32];
- private float[] mTraceY = new float[32];
- private boolean[] mTraceCurrent = new boolean[32];
- private int mTraceCount;
+ private float mCurrentX = NaN;
+ private float mCurrentY = NaN;
+ private float mPreviousX = NaN;
+ private float mPreviousY = NaN;
+ private float mFirstX = NaN;
+ private float mFirstY = NaN;
+ private boolean mPreviousPointIsHistorical;
+ private boolean mCurrentPointIsHistorical;
// True if the pointer is down.
@UnsupportedAppUsage
private boolean mCurDown;
// Most recent coordinates.
- private PointerCoords mCoords = new PointerCoords();
+ private final PointerCoords mCoords = new PointerCoords();
private int mToolType;
// Most recent velocity.
@@ -96,31 +105,20 @@ public class PointerLocationView extends View implements InputDeviceListener,
public PointerState() {
}
- public void clearTrace() {
- mTraceCount = 0;
- }
-
- public void addTrace(float x, float y, boolean current) {
- int traceCapacity = mTraceX.length;
- if (mTraceCount == traceCapacity) {
- traceCapacity *= 2;
- float[] newTraceX = new float[traceCapacity];
- System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
- mTraceX = newTraceX;
-
- float[] newTraceY = new float[traceCapacity];
- System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
- mTraceY = newTraceY;
-
- boolean[] newTraceCurrent = new boolean[traceCapacity];
- System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount);
- mTraceCurrent= newTraceCurrent;
+ void addTrace(float x, float y, boolean isHistorical) {
+ if (Float.isNaN(mFirstX)) {
+ mFirstX = x;
+ }
+ if (Float.isNaN(mFirstY)) {
+ mFirstY = y;
}
- mTraceX[mTraceCount] = x;
- mTraceY[mTraceCount] = y;
- mTraceCurrent[mTraceCount] = current;
- mTraceCount += 1;
+ mPreviousX = mCurrentX;
+ mPreviousY = mCurrentY;
+ mCurrentX = x;
+ mCurrentY = y;
+ mPreviousPointIsHistorical = mCurrentPointIsHistorical;
+ mCurrentPointIsHistorical = isHistorical;
}
}
@@ -149,6 +147,13 @@ public class PointerLocationView extends View implements InputDeviceListener,
private final SparseArray<PointerState> mPointers = new SparseArray<PointerState>();
private final PointerCoords mTempCoords = new PointerCoords();
+ // Draw the trace of all pointers in the current gesture in a separate layer
+ // that is not cleared on every frame so that we don't have to re-draw the
+ // entire trace on each frame. The trace bitmap is in the coordinate space of the unrotated
+ // display.
+ private Bitmap mTraceBitmap;
+ private final Canvas mTraceCanvas;
+
private final Region mSystemGestureExclusion = new Region();
private final Region mSystemGestureExclusionRejected = new Region();
private final Path mSystemGestureExclusionPath = new Path();
@@ -197,6 +202,9 @@ public class PointerLocationView extends View implements InputDeviceListener,
mPathPaint.setARGB(255, 0, 96, 255);
mPathPaint.setStyle(Paint.Style.STROKE);
+ mTraceCanvas = new Canvas();
+ configureTraceBitmap();
+
configureDensityDependentFactors();
mSystemGestureExclusionPaint = new Paint();
@@ -256,7 +264,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mTextPaint.getFontMetricsInt(mTextMetrics);
- mHeaderBottom = mHeaderPaddingTop-mTextMetrics.ascent+mTextMetrics.descent+2;
+ mHeaderBottom = mHeaderPaddingTop - mTextMetrics.ascent + mTextMetrics.descent + 2;
if (false) {
Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
+ " descent=" + mTextMetrics.descent
@@ -268,7 +276,8 @@ public class PointerLocationView extends View implements InputDeviceListener,
// Draw an oval. When angle is 0 radians, orients the major axis vertically,
// angles less than or greater than 0 radians rotate the major axis left or right.
- private RectF mReusableOvalRect = new RectF();
+ private final RectF mReusableOvalRect = new RectF();
+
private void drawOval(Canvas canvas, float x, float y, float major, float minor,
float angle, Paint paint) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
@@ -285,6 +294,12 @@ public class PointerLocationView extends View implements InputDeviceListener,
protected void onDraw(Canvas canvas) {
final int NP = mPointers.size();
+ // Pointer trace.
+ canvas.save();
+ rotateCanvasToUnrotatedDisplay(canvas);
+ canvas.drawBitmap(mTraceBitmap, 0, 0, null);
+ canvas.restore();
+
if (!mSystemGestureExclusion.isEmpty()) {
mSystemGestureExclusionPath.reset();
mSystemGestureExclusion.getBoundaryPath(mSystemGestureExclusionPath);
@@ -300,35 +315,14 @@ public class PointerLocationView extends View implements InputDeviceListener,
// Labels
drawLabels(canvas);
- // Pointer trace.
+ // Current pointer states.
+ canvas.save();
+ rotateCanvasToUnrotatedDisplay(canvas);
for (int p = 0; p < NP; p++) {
final PointerState ps = mPointers.valueAt(p);
+ float lastX = ps.mCurrentX, lastY = ps.mCurrentY;
- // Draw path.
- final int N = ps.mTraceCount;
- float lastX = 0, lastY = 0;
- boolean haveLast = false;
- boolean drawn = false;
- mPaint.setARGB(255, 128, 255, 255);
- for (int i=0; i < N; i++) {
- float x = ps.mTraceX[i];
- float y = ps.mTraceY[i];
- if (Float.isNaN(x) || Float.isNaN(y)) {
- haveLast = false;
- continue;
- }
- if (haveLast) {
- canvas.drawLine(lastX, lastY, x, y, mPathPaint);
- final Paint paint = ps.mTraceCurrent[i - 1] ? mCurrentPointPaint : mPaint;
- canvas.drawPoint(lastX, lastY, paint);
- drawn = true;
- }
- lastX = x;
- lastY = y;
- haveLast = true;
- }
-
- if (drawn) {
+ if (!Float.isNaN(lastX) && !Float.isNaN(lastY)) {
// Draw velocity vector.
mPaint.setARGB(255, 255, 64, 128);
float xVel = ps.mXVelocity * (1000 / 60);
@@ -353,7 +347,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
Math.max(getHeight(), getWidth()), mTargetPaint);
// Draw current point.
- int pressureLevel = (int)(ps.mCoords.pressure * 255);
+ int pressureLevel = (int) (ps.mCoords.pressure * 255);
mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
@@ -406,6 +400,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
}
}
}
+ canvas.restore();
}
private void drawLabels(Canvas canvas) {
@@ -424,8 +419,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
.append(" / ").append(mMaxNumPointers)
.toString(), 1, base, mTextPaint);
- final int count = ps.mTraceCount;
- if ((mCurDown && ps.mCurDown) || count == 0) {
+ if ((mCurDown && ps.mCurDown) || Float.isNaN(ps.mCurrentX)) {
canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
mTextBackgroundPaint);
canvas.drawText(mText.clear()
@@ -437,8 +431,8 @@ public class PointerLocationView extends View implements InputDeviceListener,
.append("Y: ").append(ps.mCoords.y, 1)
.toString(), 1 + itemW * 2, base, mTextPaint);
} else {
- float dx = ps.mTraceX[count - 1] - ps.mTraceX[0];
- float dy = ps.mTraceY[count - 1] - ps.mTraceY[0];
+ float dx = ps.mCurrentX - ps.mFirstX;
+ float dy = ps.mCurrentY - ps.mFirstY;
canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
Math.abs(dx) < mVC.getScaledTouchSlop()
? mTextBackgroundPaint : mTextLevelPaint);
@@ -565,9 +559,9 @@ public class PointerLocationView extends View implements InputDeviceListener,
.append(" TouchMinor=").append(coords.touchMinor, 3)
.append(" ToolMajor=").append(coords.toolMajor, 3)
.append(" ToolMinor=").append(coords.toolMinor, 3)
- .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
+ .append(" Orientation=").append((float) (coords.orientation * 180 / Math.PI), 1)
.append("deg")
- .append(" Tilt=").append((float)(
+ .append(" Tilt=").append((float) (
coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
.append("deg")
.append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
@@ -586,6 +580,11 @@ public class PointerLocationView extends View implements InputDeviceListener,
@Override
public void onPointerEvent(MotionEvent event) {
+ // PointerLocationView stores and draws events in the unrotated display space, so undo the
+ // event's rotation to bring it back to the unrotated display space.
+ event.transform(MotionEvent.createRotateMatrix(inverseRotation(event.getSurfaceRotation()),
+ mTraceBitmap.getWidth(), mTraceBitmap.getHeight()));
+
final int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN
@@ -598,6 +597,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
mCurNumPointers = 0;
mMaxNumPointers = 0;
mVelocity.clear();
+ mTraceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
if (mAltVelocity != null) {
mAltVelocity.clear();
}
@@ -646,7 +646,8 @@ public class PointerLocationView extends View implements InputDeviceListener,
logCoords("Pointer", action, i, coords, id, event);
}
if (ps != null) {
- ps.addTrace(coords.x, coords.y, false);
+ ps.addTrace(coords.x, coords.y, /*isHistorical*/ true);
+ updateDrawTrace(ps);
}
}
}
@@ -659,7 +660,8 @@ public class PointerLocationView extends View implements InputDeviceListener,
logCoords("Pointer", action, i, coords, id, event);
}
if (ps != null) {
- ps.addTrace(coords.x, coords.y, true);
+ ps.addTrace(coords.x, coords.y, /*isHistorical*/ false);
+ updateDrawTrace(ps);
ps.mXVelocity = mVelocity.getXVelocity(id);
ps.mYVelocity = mVelocity.getYVelocity(id);
if (mAltVelocity != null) {
@@ -702,13 +704,27 @@ public class PointerLocationView extends View implements InputDeviceListener,
if (mActivePointerId == id) {
mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
}
- ps.addTrace(Float.NaN, Float.NaN, false);
+ ps.addTrace(Float.NaN, Float.NaN, true);
}
}
invalidate();
}
+ private void updateDrawTrace(PointerState ps) {
+ mPaint.setARGB(255, 128, 255, 255);
+ float x = ps.mCurrentX;
+ float y = ps.mCurrentY;
+ float lastX = ps.mPreviousX;
+ float lastY = ps.mPreviousY;
+ if (Float.isNaN(x) || Float.isNaN(y) || Float.isNaN(lastX) || Float.isNaN(lastY)) {
+ return;
+ }
+ mTraceCanvas.drawLine(lastX, lastY, x, y, mPathPaint);
+ Paint paint = ps.mPreviousPointIsHistorical ? mPaint : mCurrentPointPaint;
+ mTraceCanvas.drawPoint(lastX, lastY, paint);
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
onPointerEvent(event);
@@ -767,7 +783,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
return true;
default:
return KeyEvent.isGamepadButton(keyCode)
- || KeyEvent.isModifierKey(keyCode);
+ || KeyEvent.isModifierKey(keyCode);
}
}
@@ -887,7 +903,7 @@ public class PointerLocationView extends View implements InputDeviceListener,
public FasterStringBuilder append(int value, int zeroPadWidth) {
final boolean negative = value < 0;
if (negative) {
- value = - value;
+ value = -value;
if (value < 0) {
append("-2147483648");
return this;
@@ -971,32 +987,34 @@ public class PointerLocationView extends View implements InputDeviceListener,
}
}
- private ISystemGestureExclusionListener mSystemGestureExclusionListener =
+ private final ISystemGestureExclusionListener mSystemGestureExclusionListener =
new ISystemGestureExclusionListener.Stub() {
- @Override
- public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion,
- Region systemGestureExclusionUnrestricted) {
- Region exclusion = Region.obtain(systemGestureExclusion);
- Region rejected = Region.obtain();
- if (systemGestureExclusionUnrestricted != null) {
- rejected.set(systemGestureExclusionUnrestricted);
- rejected.op(exclusion, Region.Op.DIFFERENCE);
- }
- Handler handler = getHandler();
- if (handler != null) {
- handler.post(() -> {
- mSystemGestureExclusion.set(exclusion);
- mSystemGestureExclusionRejected.set(rejected);
- exclusion.recycle();
- invalidate();
- });
- }
- }
- };
+ @Override
+ public void onSystemGestureExclusionChanged(int displayId,
+ Region systemGestureExclusion,
+ Region systemGestureExclusionUnrestricted) {
+ Region exclusion = Region.obtain(systemGestureExclusion);
+ Region rejected = Region.obtain();
+ if (systemGestureExclusionUnrestricted != null) {
+ rejected.set(systemGestureExclusionUnrestricted);
+ rejected.op(exclusion, Region.Op.DIFFERENCE);
+ }
+ Handler handler = getHandler();
+ if (handler != null) {
+ handler.post(() -> {
+ mSystemGestureExclusion.set(exclusion);
+ mSystemGestureExclusionRejected.set(rejected);
+ exclusion.recycle();
+ invalidate();
+ });
+ }
+ }
+ };
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ configureTraceBitmap();
configureDensityDependentFactors();
}
@@ -1008,4 +1026,58 @@ public class PointerLocationView extends View implements InputDeviceListener,
mCurrentPointPaint.setStrokeWidth(1 * mDensity);
mPathPaint.setStrokeWidth(1 * mDensity);
}
+
+ private void configureTraceBitmap() {
+ final var display = mContext.getDisplay();
+ final boolean rotated = display.getRotation() == Surface.ROTATION_90
+ || display.getRotation() == Surface.ROTATION_270;
+ int unrotatedWidth = rotated ? display.getHeight() : display.getWidth();
+ int unrotatedHeight = rotated ? display.getWidth() : display.getHeight();
+
+ if (mTraceBitmap != null && mTraceBitmap.getWidth() == unrotatedWidth
+ && mTraceBitmap.getHeight() == unrotatedHeight) {
+ return;
+ }
+ if (unrotatedWidth <= 0 || unrotatedHeight <= 0) {
+ Slog.w(TAG, "Ignoring configuration: invalid display size: " + unrotatedWidth + "x"
+ + unrotatedHeight);
+ // Initialize the bitmap to an arbitrary size. It should be reconfigured with a valid
+ // size in the future.
+ unrotatedWidth = 100;
+ unrotatedHeight = 100;
+ }
+ mTraceBitmap = Bitmap.createBitmap(unrotatedWidth, unrotatedHeight,
+ Bitmap.Config.ARGB_8888);
+ mTraceCanvas.setBitmap(mTraceBitmap);
+ }
+
+ private static int inverseRotation(@Surface.Rotation int rotation) {
+ return switch(rotation) {
+ case Surface.ROTATION_0 -> Surface.ROTATION_0;
+ case Surface.ROTATION_90 -> Surface.ROTATION_270;
+ case Surface.ROTATION_180 -> Surface.ROTATION_180;
+ case Surface.ROTATION_270 -> Surface.ROTATION_90;
+ default -> {
+ Slog.e(TAG, "Received unexpected surface rotation: " + rotation);
+ yield Surface.ROTATION_0;
+ }
+ };
+ }
+
+ private void rotateCanvasToUnrotatedDisplay(Canvas c) {
+ switch (inverseRotation(mContext.getDisplay().getRotation())) {
+ case Surface.ROTATION_90 -> {
+ c.rotate(90);
+ c.translate(0, -mTraceBitmap.getHeight());
+ }
+ case Surface.ROTATION_180 -> {
+ c.rotate(180);
+ c.translate(-mTraceBitmap.getWidth(), -mTraceBitmap.getHeight());
+ }
+ case Surface.ROTATION_270 -> {
+ c.rotate(270);
+ c.translate(-mTraceBitmap.getWidth(), 0);
+ }
+ }
+ }
}
diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp
index abc621d8dc90..4202de39adb0 100644
--- a/core/jni/android_view_WindowManagerGlobal.cpp
+++ b/core/jni/android_view_WindowManagerGlobal.cpp
@@ -69,8 +69,8 @@ void removeInputChannel(const sp<IBinder>& clientToken) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
- env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel,
- clientTokenObj.get());
+ env->CallStaticVoidMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel,
+ clientTokenObj.get());
}
int register_android_view_WindowManagerGlobal(JNIEnv* env) {
@@ -88,4 +88,4 @@ int register_android_view_WindowManagerGlobal(JNIEnv* env) {
return NO_ERROR;
}
-} // namespace android \ No newline at end of file
+} // namespace android
diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp
index 88b3e1c1ed9d..27417c0c9929 100644
--- a/core/jni/platform/host/HostRuntime.cpp
+++ b/core/jni/platform/host/HostRuntime.cpp
@@ -115,9 +115,9 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
#ifdef __linux__
{"android.content.res.ApkAssets", REG_JNI(register_android_content_res_ApkAssets)},
{"android.content.res.AssetManager", REG_JNI(register_android_content_AssetManager)},
+#endif
{"android.content.res.StringBlock", REG_JNI(register_android_content_StringBlock)},
{"android.content.res.XmlBlock", REG_JNI(register_android_content_XmlBlock)},
-#endif
{"android.database.CursorWindow", REG_JNI(register_android_database_CursorWindow)},
{"android.database.sqlite.SQLiteConnection",
REG_JNI(register_android_database_SQLiteConnection)},
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index e795e8096641..9779dc0e00b8 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -220,6 +220,15 @@ message SystemSettingsProto {
}
optional Touchpad touchpad = 36;
+ message Mouse {
+ option (android.msg_privacy).dest = DEST_EXPLICIT;
+
+ optional SettingProto reverse_vertical_scrolling = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto swap_primary_button = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ }
+
+ optional Mouse mouse = 38;
+
optional SettingProto tty_mode = 31 [ (android.privacy).dest = DEST_AUTOMATIC ];
message Vibrate {
@@ -277,5 +286,5 @@ message SystemSettingsProto {
// Please insert fields in alphabetical order and group them into messages
// if possible (to avoid reaching the method limit).
- // Next tag = 38;
+ // Next tag = 39;
}
diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java
index d850f86070bc..85ff8463725e 100644
--- a/core/tests/coretests/src/android/app/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java
@@ -60,7 +60,6 @@ public class ActivityManagerTest {
public void testProcState() throws Exception {
// For the moment mostly want to confirm we don't crash
assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE));
- assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE));
assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE));
assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE));
assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE));
diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
index b972882e68e6..cd524214e6af 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
@@ -111,12 +111,6 @@ public class ActivityManagerTest extends AndroidTestCase {
assertEquals(config.reqKeyboardType, vconfig.keyboard);
assertEquals(config.reqTouchScreen, vconfig.touchscreen);
assertEquals(config.reqNavigation, vconfig.navigation);
- if (vconfig.navigation == Configuration.NAVIGATION_NONAV) {
- assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV);
- }
- if (vconfig.keyboard != Configuration.KEYBOARD_UNDEFINED) {
- assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD);
- }
}
@SmallTest
diff --git a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java
index a28b2f66aaa8..51e79e7ac4e1 100644
--- a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java
+++ b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java
@@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import android.app.assist.AssistStructure.ViewNode;
import android.app.assist.AssistStructure.ViewNodeBuilder;
@@ -37,6 +38,7 @@ import android.os.Bundle;
import android.os.LocaleList;
import android.os.OutcomeReceiver;
import android.os.Parcel;
+import android.os.PooledStringWriter;
import android.os.SystemClock;
import android.text.InputFilter;
import android.util.Log;
@@ -355,6 +357,18 @@ public class AssistStructureTest {
}
+ @Test
+ public void testParcelTransferWriter_writeNull() {
+ AssistStructure structure = new AssistStructure(mActivity, FOR_AUTOFILL, NO_FLAGS);
+ Parcel parcel = Parcel.obtain();
+ AssistStructure.ParcelTransferWriter writer =
+ new AssistStructure.ParcelTransferWriter(structure, parcel);
+ writer.writeView(null, parcel, new PooledStringWriter(parcel), 0);
+
+ // No throw any exception.
+ assertTrue(true);
+ }
+
private EditText newSmallView() {
EditText view = new EditText(mContext);
view.setText("I AM GROOT");
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
index 6e563ff44478..da202b63d0f1 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java
@@ -46,7 +46,7 @@ public class AccessibilityNodeInfoTest {
// The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest:
// See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo,
// and assertAccessibilityNodeInfoCleared in that class.
- private static final int NUM_MARSHALLED_PROPERTIES = 44;
+ private static final int NUM_MARSHALLED_PROPERTIES = 45;
/**
* The number of properties that are purposely not marshalled
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
new file mode 100644
index 000000000000..6419c1e07f2e
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification.ProgressStyle;
+import android.graphics.Color;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.widget.NotificationProgressDrawable.Part;
+import com.android.internal.widget.NotificationProgressDrawable.Point;
+import com.android.internal.widget.NotificationProgressDrawable.Segment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationProgressBarTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_segmentsIsEmpty() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_segmentLengthIsNegative() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(-50));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_segmentLengthIsZero() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(0));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_progressIsNegative() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = -50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_progressIsZero() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 0;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ int fadedRed = 0x7FFF0000;
+ List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_progressAtMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.RED));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 100;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_progressAboveMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 150;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionIsNegative() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(-50).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionIsZero() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(0).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionAtMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(100).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void processAndConvertToDrawableParts_pointPositionAboveMax() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(150).setColor(Color.RED));
+ int progress = 50;
+ boolean isStyledByProgress = true;
+
+ NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress,
+ isStyledByProgress);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ int progress = 60;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ // Colors with 50% opacity
+ int fadedGreen = 0x7F00FF00;
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.50f, Color.RED),
+ new Segment(0.10f, Color.GREEN),
+ new Segment(0.40f, fadedGreen, true)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_singleSegmentWithPoints() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(15).setColor(Color.RED));
+ points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
+ int progress = 60;
+ boolean isStyledByProgress = true;
+
+ // Colors with 50% opacity
+ int fadedBlue = 0x7F0000FF;
+ int fadedYellow = 0x7FFFFF00;
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.15f, Color.BLUE),
+ new Point(null, Color.RED),
+ new Segment(0.10f, Color.BLUE),
+ new Point(null, Color.BLUE),
+ new Segment(0.35f, Color.BLUE),
+ new Point(null, Color.BLUE),
+ new Segment(0.15f, fadedBlue, true),
+ new Point(null, fadedYellow, true),
+ new Segment(0.25f, fadedBlue, true)));
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(15).setColor(Color.RED));
+ points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(60).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
+ int progress = 60;
+ boolean isStyledByProgress = true;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ // Colors with 50% opacity
+ int fadedGreen = 0x7F00FF00;
+ int fadedBlue = 0x7F0000FF;
+ int fadedYellow = 0x7FFFFF00;
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.15f, Color.RED),
+ new Point(null, Color.RED),
+ new Segment(0.10f, Color.RED),
+ new Point(null, Color.BLUE),
+ new Segment(0.25f, Color.RED),
+ new Segment(0.10f, Color.GREEN),
+ new Point(null, Color.BLUE),
+ new Segment(0.15f, fadedGreen, true),
+ new Point(null, fadedYellow, true),
+ new Segment(0.25f, fadedGreen, true)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+
+ @Test
+ public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() {
+ List<ProgressStyle.Segment> segments = new ArrayList<>();
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.RED));
+ segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN));
+ List<ProgressStyle.Point> points = new ArrayList<>();
+ points.add(new ProgressStyle.Point(15).setColor(Color.RED));
+ points.add(new ProgressStyle.Point(25).setColor(Color.BLUE));
+ points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW));
+ int progress = 60;
+ boolean isStyledByProgress = false;
+
+ List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
+ segments, points, progress, isStyledByProgress);
+
+ List<Part> expected = new ArrayList<>(List.of(
+ new Segment(0.15f, Color.RED),
+ new Point(null, Color.RED),
+ new Segment(0.10f, Color.RED),
+ new Point(null, Color.BLUE),
+ new Segment(0.25f, Color.RED),
+ new Segment(0.25f, Color.GREEN),
+ new Point(null, Color.YELLOW),
+ new Segment(0.25f, Color.GREEN)));
+
+ assertThat(parts).isEqualTo(expected);
+ }
+}
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 3b739c3d5817..1260796810c2 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -24,6 +24,7 @@
<uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" />
+ <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" />
<application>
<activity
diff --git a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml
index 2b2e9df07dce..2b2e9df07dce 100644
--- a/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_dismiss_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/open_by_default_settings_dialog_confirm_button_background.xml
diff --git a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
index 8ff382bbc7b4..b5bceda9a623 100644
--- a/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
+++ b/libs/WindowManager/Shell/res/layout/open_by_default_settings_dialog.xml
@@ -111,7 +111,7 @@
</RadioGroup>
<Button
- android:id="@+id/open_by_default_settings_dialog_dismiss_button"
+ android:id="@+id/open_by_default_settings_dialog_confirm_button"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="@string/open_by_default_dialog_dismiss_button_text"
@@ -122,7 +122,7 @@
android:textSize="14sp"
android:textFontWeight="500"
android:textColor="?androidprv:attr/materialColorOnPrimary"
- android:background="@drawable/open_by_default_settings_dialog_dismiss_button_background"/>
+ android:background="@drawable/open_by_default_settings_dialog_confirm_button_background"/>
</LinearLayout>
</ScrollView>
</FrameLayout>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 71bcb590ae23..65132fe89063 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -22,7 +22,13 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
+import android.content.pm.verify.domain.DomainVerificationManager
+import android.content.pm.verify.domain.DomainVerificationUserState
import android.net.Uri
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+
+private const val TAG = "AppToWebUtils"
private val GenericBrowserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
@@ -58,4 +64,25 @@ fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? {
val component = intent.resolveActivity(packageManager) ?: return null
intent.setComponent(component)
return intent
-} \ No newline at end of file
+}
+
+/**
+ * Returns the [DomainVerificationUserState] of the user associated with the given
+ * [DomainVerificationManager] and the given package.
+ */
+fun getDomainVerificationUserState(
+ manager: DomainVerificationManager,
+ packageName: String
+): DomainVerificationUserState? {
+ try {
+ return manager.getDomainVerificationUserState(packageName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ ProtoLog.w(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "%s: Failed to get domain verification user state: %s",
+ TAG,
+ e.message!!
+ )
+ return null
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
index 4926cbdbe9fb..a727b54b3a3f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialog.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.apptoweb
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
+import android.content.pm.verify.domain.DomainVerificationManager
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.view.LayoutInflater
@@ -30,6 +31,7 @@ import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
import android.view.WindowlessWindowManager
import android.widget.ImageView
+import android.widget.RadioButton
import android.widget.TextView
import android.window.TaskConstants
import com.android.wm.shell.R
@@ -58,8 +60,17 @@ internal class OpenByDefaultDialog(
private lateinit var appIconView: ImageView
private lateinit var appNameView: TextView
+ private lateinit var openInAppButton: RadioButton
+ private lateinit var openInBrowserButton: RadioButton
+
+ private val domainVerificationManager =
+ context.getSystemService(DomainVerificationManager::class.java)!!
+ private val packageName = taskInfo.baseActivity?.packageName!!
+
+
init {
createDialog()
+ initializeRadioButtons()
bindAppInfo(appIconBitmap, appName)
}
@@ -111,9 +122,30 @@ internal class OpenByDefaultDialog(
closeMenu()
}
+ dialog.setConfirmButtonClickListener {
+ setDefaultLinkHandlingSetting()
+ closeMenu()
+ }
+
listener.onDialogCreated()
}
+ private fun initializeRadioButtons() {
+ openInAppButton = dialog.requireViewById(R.id.open_in_app_button)
+ openInBrowserButton = dialog.requireViewById(R.id.open_in_browser_button)
+
+ val userState =
+ getDomainVerificationUserState(domainVerificationManager, packageName) ?: return
+ val openInApp = userState.isLinkHandlingAllowed
+ openInAppButton.isChecked = openInApp
+ openInBrowserButton.isChecked = !openInApp
+ }
+
+ private fun setDefaultLinkHandlingSetting() {
+ domainVerificationManager.setDomainVerificationLinkHandlingAllowed(
+ packageName, openInAppButton.isChecked)
+ }
+
private fun closeMenu() {
dialogContainer?.releaseView()
dialogContainer = null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
index d03a38e8699a..1b914f419d94 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/OpenByDefaultDialogView.kt
@@ -36,9 +36,6 @@ class OpenByDefaultDialogView @JvmOverloads constructor(
private lateinit var backgroundDim: Drawable
fun setDismissOnClickListener(callback: (View) -> Unit) {
- val dismissButton = dialogContainer.requireViewById<Button>(
- R.id.open_by_default_settings_dialog_dismiss_button)
- dismissButton.setOnClickListener(callback)
// Clicks on the background dim should also dismiss the dialog.
setOnClickListener(callback)
// We add a no-op on-click listener to the dialog container so that clicks on it won't
@@ -46,6 +43,13 @@ class OpenByDefaultDialogView @JvmOverloads constructor(
dialogContainer.setOnClickListener { }
}
+ fun setConfirmButtonClickListener(callback: (View) -> Unit) {
+ val dismissButton = dialogContainer.requireViewById<Button>(
+ R.id.open_by_default_settings_dialog_confirm_button
+ )
+ dismissButton.setOnClickListener(callback)
+ }
+
override fun onFinishInflate() {
super.onFinishInflate()
dialogContainer = requireViewById(R.id.open_by_default_dialog_container)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
index b3491baa629d..b83b5f341dda 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -177,26 +177,84 @@ object PipUtils {
}
/**
+ * Calculates the transform to apply on a UNTRANSFORMED (config-at-end) Activity surface in
+ * order for it's hint-rect to occupy the same task-relative position/dimensions as it would
+ * have at the end of the transition (post-configuration).
+ *
+ * This is intended to be used in tandem with [calcStartTransform] below applied to the parent
+ * task. Applying both transforms simultaneously should result in the appearance of nothing
+ * having happened yet.
+ *
+ * Only the task should be animated (into it's identity state) and then WMCore will reset the
+ * activity transform in sync with its new configuration upon finish.
+ *
+ * Usage example:
+ * calcEndTransform(pipActivity, pipTask, scale, pos);
+ * t.setScale(pipActivity.getLeash(), scale.x, scale.y);
+ * t.setPosition(pipActivity.getLeash(), pos.x, pos.y);
+ *
+ * @see calcStartTransform
+ */
+ @JvmStatic
+ fun calcEndTransform(pipActivity: TransitionInfo.Change, pipTask: TransitionInfo.Change,
+ outScale: PointF, outPos: PointF) {
+ val actStartBounds = pipActivity.startAbsBounds
+ val actEndBounds = pipActivity.endAbsBounds
+ val taskEndBounds = pipTask.endAbsBounds
+
+ var hintRect = pipTask.taskInfo?.pictureInPictureParams?.sourceRectHint
+ if (hintRect == null) {
+ hintRect = Rect(actStartBounds)
+ hintRect.offsetTo(0, 0)
+ }
+
+ // FA = final activity bounds (absolute)
+ // FT = final task bounds (absolute)
+ // SA = start activity bounds (absolute)
+ // H = source hint (relative to start activity bounds)
+ // We want to transform the activity so that when the task is at FT, H overlaps with FA
+
+ // This scales the activity such that the hint rect has the same dimensions
+ // as the final activity bounds.
+ val hintToEndScaleX = (actEndBounds.width().toFloat()) / (hintRect.width().toFloat())
+ val hintToEndScaleY = (actEndBounds.height().toFloat()) / (hintRect.height().toFloat())
+ // top-left needs to be (FA.tl - FT.tl) - H.tl * hintToEnd . H is relative to the
+ // activity; so, for example, if shrinking H to FA (hintToEnd < 1), then the tl of the
+ // shrunk SA is closer to H than expected, so we need to reduce how much we offset SA
+ // to get H.tl to match.
+ val startActPosInTaskEndX =
+ (actEndBounds.left - taskEndBounds.left) - hintRect.left * hintToEndScaleX
+ val startActPosInTaskEndY =
+ (actEndBounds.top - taskEndBounds.top) - hintRect.top * hintToEndScaleY
+ outScale.set(hintToEndScaleX, hintToEndScaleY)
+ outPos.set(startActPosInTaskEndX, startActPosInTaskEndY)
+ }
+
+ /**
* Calculates the transform and crop to apply on a Task surface in order for the config-at-end
* activity inside it (original-size activity transformed to match it's hint rect to the final
* Task bounds) to occupy the same world-space position/dimensions as it had before the
* transition.
*
+ * Intended to be used in tandem with [calcEndTransform].
+ *
* Usage example:
- * calcStartTransform(pipChange, scale, pos, crop);
- * t.setScale(pipChange.getLeash(), scale.x, scale.y);
- * t.setPosition(pipChange.getLeash(), pos.x, pos.y);
- * t.setCrop(pipChange.getLeash(), crop);
+ * calcStartTransform(pipTask, scale, pos, crop);
+ * t.setScale(pipTask.getLeash(), scale.x, scale.y);
+ * t.setPosition(pipTask.getLeash(), pos.x, pos.y);
+ * t.setCrop(pipTask.getLeash(), crop);
+ *
+ * @see calcEndTransform
*/
@JvmStatic
- fun calcStartTransform(pipChange: TransitionInfo.Change, outScale: PointF,
+ fun calcStartTransform(pipTask: TransitionInfo.Change, outScale: PointF,
outPos: PointF, outCrop: Rect) {
- val startBounds = pipChange.startAbsBounds
- val taskEndBounds = pipChange.endAbsBounds
+ val startBounds = pipTask.startAbsBounds
+ val taskEndBounds = pipTask.endAbsBounds
// For now, pip activity bounds always matches task bounds. If this ever changes, we'll
// need to get the activity offset.
val endBounds = taskEndBounds
- var hintRect = pipChange.taskInfo?.pictureInPictureParams?.sourceRectHint
+ var hintRect = pipTask.taskInfo?.pictureInPictureParams?.sourceRectHint
if (hintRect == null) {
hintRect = Rect(startBounds)
hintRect.offsetTo(0, 0)
@@ -226,8 +284,8 @@ object PipUtils {
+ startBounds.left + hintRect.left)
val endTaskPosForStartY = (-(endBounds.top - taskEndBounds.top) * endToHintScaleY
+ startBounds.top + hintRect.top)
- outScale[endToHintScaleX] = endToHintScaleY
- outPos[endTaskPosForStartX] = endTaskPosForStartY
+ outScale.set(endToHintScaleX, endToHintScaleY)
+ outPos.set(endTaskPosForStartX, endTaskPosForStartY)
// now need to set crop to reveal the non-hint stuff. Again, hintrect is relative, so
// we must apply outsets to reveal the *activity* content which is *inside* the task
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 75adef4d4327..52262e68c401 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
@@ -264,7 +264,8 @@ public abstract class WMShellModule {
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
- Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) {
+ Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
+ FocusTransitionObserver focusTransitionObserver) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return new DesktopModeWindowDecorViewModel(
context,
@@ -291,7 +292,8 @@ public abstract class WMShellModule {
desktopTasksLimiter,
appHandleEducationController,
windowDecorCaptionHandleRepository,
- desktopActivityOrientationHandler);
+ desktopActivityOrientationHandler,
+ focusTransitionObserver);
}
return new CaptionWindowDecorViewModel(
context,
@@ -305,7 +307,8 @@ public abstract class WMShellModule {
displayController,
rootTaskDisplayAreaOrganizer,
syncQueue,
- transitions);
+ transitions,
+ focusTransitionObserver);
}
@WMSingleton
@@ -695,10 +698,16 @@ public abstract class WMShellModule {
static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
Context context,
Transitions transitions,
- @DynamicOverride DesktopRepository desktopRepository) {
+ @DynamicOverride DesktopRepository desktopRepository,
+ DisplayController displayController,
+ ShellTaskOrganizer shellTaskOrganizer) {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(
- new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
+ new DesktopFullImmersiveTransitionHandler(
+ transitions,
+ desktopRepository,
+ displayController,
+ shellTaskOrganizer));
}
return Optional.empty();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
index f749aa1edd92..679179a7ff68 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -27,8 +27,12 @@ import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
-import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -41,16 +45,29 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
class DesktopFullImmersiveTransitionHandler(
private val transitions: Transitions,
private val desktopRepository: DesktopRepository,
+ private val displayController: DisplayController,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
private val transactionSupplier: () -> SurfaceControl.Transaction,
) : TransitionHandler {
constructor(
transitions: Transitions,
desktopRepository: DesktopRepository,
- ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })
+ displayController: DisplayController,
+ shellTaskOrganizer: ShellTaskOrganizer,
+ ) : this(
+ transitions,
+ desktopRepository,
+ displayController,
+ shellTaskOrganizer,
+ { SurfaceControl.Transaction() }
+ )
private var state: TransitionState? = null
+ @VisibleForTesting
+ val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>()
+
/** Whether there is an immersive transition that hasn't completed yet. */
private val inProgress: Boolean
get() = state != null
@@ -61,15 +78,15 @@ class DesktopFullImmersiveTransitionHandler(
var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null
/** Starts a transition to enter full immersive state inside the desktop. */
- fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ fun moveTaskToImmersive(taskInfo: RunningTaskInfo) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "FullImmersive: cannot start entry because transition already in progress."
- )
+ logV("Cannot start entry because transition already in progress.")
return
}
-
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, Rect())
+ }
+ logV("Moving task ${taskInfo.taskId} into immersive mode")
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
state = TransitionState(
transition = transition,
@@ -79,15 +96,18 @@ class DesktopFullImmersiveTransitionHandler(
)
}
- fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
+ fun moveTaskToNonImmersive(taskInfo: RunningTaskInfo) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "$TAG: cannot start exit because transition already in progress."
- )
+ logV("Cannot start exit because transition already in progress.")
return
}
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val destinationBounds = calculateMaximizeBounds(displayLayout, taskInfo)
+ val wct = WindowContainerTransaction().apply {
+ setBounds(taskInfo.token, destinationBounds)
+ }
+ logV("Moving task ${taskInfo.taskId} out of immersive mode")
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
state = TransitionState(
transition = transition,
@@ -97,6 +117,82 @@ class DesktopFullImmersiveTransitionHandler(
)
}
+ /**
+ * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
+ *
+ * @param transition that will apply this transaction
+ * @param wct that will apply these changes
+ * @param displayId of the display that should exit immersive mode
+ */
+ fun exitImmersiveIfApplicable(
+ transition: IBinder,
+ wct: WindowContainerTransaction,
+ displayId: Int
+ ) {
+ if (!Flags.enableFullyImmersiveInDesktop()) return
+ exitImmersiveIfApplicable(wct, displayId)?.invoke(transition)
+ }
+
+ /**
+ * Bring the immersive app of the given [displayId] out of immersive mode, if applicable.
+ *
+ * @param wct that will apply these changes
+ * @param displayId of the display that should exit immersive mode
+ * @return a function to apply once the transition that will apply these changes is started
+ */
+ fun exitImmersiveIfApplicable(
+ wct: WindowContainerTransaction,
+ displayId: Int
+ ): ((IBinder) -> Unit)? {
+ if (!Flags.enableFullyImmersiveInDesktop()) return null
+ val displayLayout = displayController.getDisplayLayout(displayId) ?: return null
+ val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null
+ val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null
+ logV("Appending immersive exit for task: $immersiveTask in display: $displayId")
+ wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
+ return { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) }
+ }
+
+ /**
+ * Bring the given [taskInfo] out of immersive mode, if applicable.
+ *
+ * @param wct that will apply these changes
+ * @param taskInfo of the task that should exit immersive mode
+ * @return a function to apply once the transition that will apply these changes is started
+ */
+ fun exitImmersiveIfApplicable(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo
+ ): ((IBinder) -> Unit)? {
+ if (!Flags.enableFullyImmersiveInDesktop()) return null
+ if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) {
+ // A full immersive task is being minimized, make sure the immersive state is broken
+ // (i.e. resize back to max bounds).
+ displayController.getDisplayLayout(taskInfo.displayId)?.let { displayLayout ->
+ wct.setBounds(taskInfo.token, calculateMaximizeBounds(displayLayout, taskInfo))
+ logV("Appending immersive exit for task: ${taskInfo.taskId}")
+ return { transition ->
+ addPendingImmersiveExit(
+ taskId = taskInfo.taskId,
+ displayId = taskInfo.displayId,
+ transition = transition
+ )
+ }
+ }
+ }
+ return null
+ }
+
+ private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) {
+ pendingExternalExitTransitions.add(
+ ExternalPendingExit(
+ taskId = taskId,
+ displayId = displayId,
+ transition = transition
+ )
+ )
+ }
+
override fun startAnimation(
transition: IBinder,
info: TransitionInfo,
@@ -190,15 +286,31 @@ class DesktopFullImmersiveTransitionHandler(
* Called when any transition in the system is ready to play. This is needed to update the
* repository state before window decorations are drawn (which happens immediately after
* |onTransitionReady|, before this transition actually animates) because drawing decorations
- * depends in whether the task is in full immersive state or not.
+ * depends on whether the task is in full immersive state or not.
*/
- fun onTransitionReady(transition: IBinder) {
+ fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
+ // Check if this is a pending external exit transition.
+ val pendingExit = pendingExternalExitTransitions
+ .firstOrNull { pendingExit -> pendingExit.transition == transition }
+ if (pendingExit != null) {
+ pendingExternalExitTransitions.remove(pendingExit)
+ if (info.hasTaskChange(taskId = pendingExit.taskId)) {
+ if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) {
+ logV("Pending external exit for task ${pendingExit.taskId} verified")
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = pendingExit.displayId,
+ taskId = pendingExit.taskId,
+ immersive = false
+ )
+ }
+ }
+ return
+ }
+
+ // Check if this is a direct immersive enter/exit transition.
val state = this.state ?: return
- // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
- // immersive, which isn't realistic. The app could crash, the user could dismiss it from
- // overview, etc. This (or its caller) should search all transitions to look for any
- // immersive task exiting that state to keep the repository properly updated.
if (transition == state.transition) {
+ logV("Direct move for task ${state.taskId} in ${state.direction} direction verified")
when (state.direction) {
Direction.ENTER -> {
desktopRepository.setTaskInFullImmersiveState(
@@ -225,6 +337,9 @@ class DesktopFullImmersiveTransitionHandler(
private fun requireState(): TransitionState =
state ?: error("Expected non-null transition state")
+ private fun TransitionInfo.hasTaskChange(taskId: Int): Boolean =
+ changes.any { c -> c.taskInfo?.taskId == taskId }
+
/** The state of the currently running transition. */
private data class TransitionState(
val transition: IBinder,
@@ -233,12 +348,28 @@ class DesktopFullImmersiveTransitionHandler(
val direction: Direction
)
+ /**
+ * Tracks state of a transition involving an immersive exit that is external to this class' own
+ * transitions. This usually means transitions that exit immersive mode as a side-effect and
+ * not the primary action (for example, minimizing the immersive task or launching a new task
+ * on top of the immersive task).
+ */
+ data class ExternalPendingExit(
+ val taskId: Int,
+ val displayId: Int,
+ val transition: IBinder,
+ )
+
private enum class Direction {
ENTER, EXIT
}
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
private companion object {
- private const val TAG = "FullImmersiveHandler"
+ private const val TAG = "DesktopImmersive"
private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 5a277316ffd4..379e052e7b38 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -156,6 +156,21 @@ class DesktopModeEventLogger {
)
}
+ fun logTaskInfoStateInit() {
+ logTaskUpdate(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
+ /* session_id */ 0,
+ TaskUpdate(
+ visibleTaskCount = 0,
+ instanceId = 0,
+ uid = 0,
+ taskHeight = 0,
+ taskWidth = 0,
+ taskX = 0,
+ taskY = 0)
+ )
+ }
+
private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) {
FrameworkStatsLog.write(
DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index b8507e3b2764..f847aa8918c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -102,6 +102,7 @@ class DesktopModeLoggerTransitionObserver(
SystemProperties.set(
VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE)
+ desktopModeEventLogger.logTaskInfoStateInit()
}
override fun onTransitionReady(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index bd6172226cf2..6d4792250be2 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
@@ -123,6 +123,29 @@ fun calculateInitialBounds(
}
/**
+ * Calculates the maximized bounds of a task given in the given [DisplayLayout], taking
+ * resizability into consideration.
+ */
+fun calculateMaximizeBounds(
+ displayLayout: DisplayLayout,
+ taskInfo: RunningTaskInfo,
+): Rect {
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+ if (taskInfo.isResizeable) {
+ // if resizable then expand to entire stable bounds (full display minus insets)
+ return Rect(stableBounds)
+ } else {
+ // if non-resizable then calculate max bounds according to aspect ratio
+ val activityAspectRatio = calculateAspectRatio(taskInfo)
+ val newSize = maximizeSizeGivenAspectRatio(taskInfo,
+ Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
+ return centerInArea(
+ newSize, stableBounds, stableBounds.left, stableBounds.top)
+ }
+}
+
+/**
* Calculates the largest size that can fit in a given area while maintaining a specific aspect
* ratio.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index c175133dd37b..5ac4ef5cf049 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -328,6 +328,10 @@ class DesktopRepository (
return desktopTaskDataSequence().any { taskId == it.fullImmersiveTaskId }
}
+ /** Returns the task that is currently in immersive mode in this display, or null. */
+ fun getTaskInFullImmersiveState(displayId: Int): Int? =
+ desktopTaskDataByDisplayId.getOrCreate(displayId).fullImmersiveTaskId
+
private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
visibleTasksListeners.forEach { (listener, executor) ->
executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
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 75c795b70378..92535f37094a 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
@@ -91,6 +91,7 @@ import com.android.wm.shell.shared.ShellSharedConstants
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity
@@ -190,6 +191,7 @@ class DesktopTasksController(
private var recentsAnimationRunning = false
private lateinit var splitScreenController: SplitScreenController
+ lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
// Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
@@ -354,6 +356,8 @@ class DesktopTasksController(
// TODO(342378842): Instead of using default display, support multiple displays
val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
DEFAULT_DISPLAY, wct, taskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
wct.startTask(
taskId,
ActivityOptions.makeBasic().apply {
@@ -363,6 +367,7 @@ class DesktopTasksController(
// TODO(343149901): Add DPI changes for task launch
val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
return true
}
@@ -379,6 +384,7 @@ class DesktopTasksController(
}
logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
exitSplitIfApplicable(wct, task)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, task.displayId)
// Bring other apps to front first
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -386,6 +392,7 @@ class DesktopTasksController(
val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/**
@@ -422,8 +429,13 @@ class DesktopTasksController(
val taskToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
addMoveToDesktopChanges(wct, taskInfo)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+ wct, taskInfo.displayId)
val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
- transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
+ transition?.let {
+ addPendingMinimizeTransition(it, taskToMinimize)
+ runOnTransit?.invoke(transition)
+ }
}
/**
@@ -453,20 +465,36 @@ class DesktopTasksController(
removeWallpaperActivity(wct)
}
taskRepository.addClosingTask(displayId, taskId)
+ taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
+ doesAnyTaskRequireTaskbarRounding(
+ displayId,
+ taskId
+ )
+ )
}
- /**
- * Perform clean up of the desktop wallpaper activity if the minimized window task is the last
- * active task.
- *
- * @param wct transaction to modify if the last active task is minimized
- * @param taskId task id of the window that's being minimized
- */
- fun onDesktopWindowMinimize(wct: WindowContainerTransaction, taskId: Int) {
+ fun minimizeTask(taskInfo: RunningTaskInfo) {
+ val taskId = taskInfo.taskId
+ val displayId = taskInfo.displayId
+ val wct = WindowContainerTransaction()
if (taskRepository.isOnlyVisibleNonClosingTask(taskId)) {
+ // Perform clean up of the desktop wallpaper activity if the minimized window task is
+ // the last active task.
removeWallpaperActivity(wct)
}
- // Do not call taskRepository.minimizeTask because it will be called by DekstopTasksLimiter.
+ // Notify immersive handler as it might need to exit immersive state.
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+
+ wct.reorder(taskInfo.token, false)
+ val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
+ desktopTasksLimiter.ifPresent {
+ it.addPendingMinimizeChange(
+ transition = transition,
+ displayId = displayId,
+ taskId = taskId
+ )
+ }
+ runOnTransit?.invoke(transition)
}
/** Move a task with given `taskId` to fullscreen */
@@ -552,6 +580,8 @@ class DesktopTasksController(
// TODO: b/342378842 - Instead of using default display, support multiple displays
val taskToMinimize: RunningTaskInfo? =
addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
+ val runOnTransit = immersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY)
wct.startTask(
taskId,
ActivityOptions.makeBasic().apply {
@@ -560,6 +590,7 @@ class DesktopTasksController(
)
val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/** Move a task to the front */
@@ -567,11 +598,14 @@ class DesktopTasksController(
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
+ val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+ wct, taskInfo.displayId)
val taskToMinimize =
addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)
val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
+ runOnTransit?.invoke(transition)
}
/**
@@ -643,22 +677,12 @@ class DesktopTasksController(
private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
check(taskInfo.isFreeform) { "Task must already be in freeform" }
- val wct = WindowContainerTransaction().apply {
- setBounds(taskInfo.token, Rect())
- }
- immersiveTransitionHandler.enterImmersive(taskInfo, wct)
+ immersiveTransitionHandler.moveTaskToImmersive(taskInfo)
}
private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
check(taskInfo.isFreeform) { "Task must already be in freeform" }
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
- val stableBounds = Rect().apply { displayLayout.getStableBounds(this) }
- val destinationBounds = getMaximizeBounds(taskInfo, stableBounds)
-
- val wct = WindowContainerTransaction().apply {
- setBounds(taskInfo.token, destinationBounds)
- }
- immersiveTransitionHandler.exitImmersive(taskInfo, wct)
+ immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo)
}
/**
@@ -697,7 +721,7 @@ class DesktopTasksController(
// and toggle to the stable bounds.
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
- destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds))
+ destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
@@ -1285,8 +1309,10 @@ class DesktopTasksController(
if (useDesktopOverrideDensity()) {
wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE)
}
- // Desktop Mode is showing and we're launching a new Task - we might need to minimize
- // a Task.
+ // Desktop Mode is showing and we're launching a new Task:
+ // 1) Exit immersive if needed.
+ immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId)
+ // 2) minimize a Task if needed.
val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
if (taskToMinimize != null) {
addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1316,6 +1342,9 @@ class DesktopTasksController(
val taskToMinimize =
addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
addPendingMinimizeTransition(transition, taskToMinimize)
+ immersiveTransitionHandler.exitImmersiveIfApplicable(
+ transition, wct, task.displayId
+ )
}
}
return null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index a4bc2fe9460b..0b1bb8f36fa8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -21,6 +21,7 @@ import android.content.Context
import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
@@ -36,8 +37,8 @@ import com.android.wm.shell.transition.Transitions
/**
* A [Transitions.TransitionObserver] that observes shell transitions and updates the
- * [DesktopRepository] state TODO: b/332682201 This observes transitions related to desktop
- * mode and other transitions that originate both within and outside shell.
+ * [DesktopRepository] state TODO: b/332682201 This observes transitions related to desktop mode and
+ * other transitions that originate both within and outside shell.
*/
class DesktopTasksTransitionObserver(
private val context: Context,
@@ -47,6 +48,8 @@ class DesktopTasksTransitionObserver(
shellInit: ShellInit
) : Transitions.TransitionObserver {
+ private var transitionToCloseWallpaper: IBinder? = null
+
init {
if (DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback(::onInit, this)
@@ -70,6 +73,7 @@ class DesktopTasksTransitionObserver(
handleBackNavigation(info)
removeTaskIfNeeded(info)
}
+ removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
}
private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -81,13 +85,9 @@ class DesktopTasksTransitionObserver(
val taskInfo = change.taskInfo
if (taskInfo == null || taskInfo.taskId == -1) continue
- if (desktopRepository.isActiveTask(taskInfo.taskId)
- && taskInfo.windowingMode != WINDOWING_MODE_FREEFORM
- ) {
- desktopRepository.removeFreeformTask(
- taskInfo.displayId,
- taskInfo.taskId
- )
+ if (desktopRepository.isActiveTask(taskInfo.taskId) &&
+ taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) {
+ desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId)
}
}
}
@@ -104,14 +104,32 @@ class DesktopTasksTransitionObserver(
if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) > 0 &&
change.mode == TRANSIT_TO_BACK &&
- taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
- ) {
+ taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
}
}
}
}
+ private fun removeWallpaperOnLastTaskClosingIfNeeded(
+ transition: IBinder,
+ info: TransitionInfo
+ ) {
+ for (change in info.changes) {
+ val taskInfo = change.taskInfo
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue
+ }
+
+ if (desktopRepository.getVisibleTaskCount(taskInfo.displayId) == 1 &&
+ change.mode == TRANSIT_CLOSE &&
+ taskInfo.windowingMode == WINDOWING_MODE_FREEFORM &&
+ desktopRepository.wallpaperActivityToken != null) {
+ transitionToCloseWallpaper = transition
+ }
+ }
+ }
+
override fun onTransitionStarting(transition: IBinder) {
// TODO: b/332682201 Update repository state
}
@@ -122,6 +140,16 @@ class DesktopTasksTransitionObserver(
override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
// TODO: b/332682201 Update repository state
+ if (transitionToCloseWallpaper == transition) {
+ // TODO: b/362469671 - Handle merging the animation when desktop is also closing.
+ desktopRepository.wallpaperActivityToken?.let { wallpaperActivityToken ->
+ transitions.startTransition(
+ TRANSIT_CLOSE,
+ WindowContainerTransaction().removeTask(wallpaperActivityToken),
+ null)
+ }
+ transitionToCloseWallpaper = null
+ }
}
private fun updateWallpaperToken(info: TransitionInfo) {
@@ -139,10 +167,9 @@ class DesktopTasksTransitionObserver(
// task.
shellTaskOrganizer.applyTransaction(
WindowContainerTransaction()
- .setTaskTrimmableFromRecents(taskInfo.token, false)
- )
+ .setTaskTrimmableFromRecents(taskInfo.token, false))
}
- WindowManager.TRANSIT_CLOSE ->
+ TRANSIT_CLOSE ->
desktopRepository.wallpaperActivityToken = null
else -> {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index 1090a4690a5d..86351e364cdd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -51,5 +51,5 @@ interface IDesktopMode {
void moveToDesktop(int taskId, in DesktopModeTransitionSource transitionSource);
/** Remove desktop on the given display */
- void removeDesktop(int displayId);
+ oneway void removeDesktop(int displayId);
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index ae65892ef6c1..a16446fffa15 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -126,6 +126,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
|| repository.isClosingTask(taskInfo.taskId)) {
// A task that's vanishing should be removed:
// - If it's closed by the X button which means it's marked as a closing task.
+ repository.removeClosingTask(taskInfo.taskId);
repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
} else {
repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false);
@@ -150,8 +151,6 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
mDesktopRepository.ifPresent(repository -> {
if (taskInfo.isVisible) {
repository.addActiveTask(taskInfo.displayId, taskInfo.taskId);
- } else if (repository.isClosingTask(taskInfo.taskId)) {
- repository.removeClosingTask(taskInfo.taskId);
}
repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId,
taskInfo.isVisible);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 4106a10996ed..771573d48e45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -89,7 +89,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs
// TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
// is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
// Otherwise window decoration relayout won't run with the immersive state up to date.
- mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition));
+ mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition, info));
}
// Update focus state first to ensure the correct state can be queried from listeners.
// TODO(371503964): Remove this once the unified task repository is ready.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index c540edef32c4..be4fd7c5eeec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -58,9 +58,11 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
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.extension.TaskInfoKt;
@@ -68,7 +70,7 @@ import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
* View model for the window decoration with a caption and shadows. Works with
* {@link CaptionWindowDecoration}.
*/
-public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
+public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusTransitionListener {
private static final String TAG = "CaptionWindowDecorViewModel";
private final ShellTaskOrganizer mTaskOrganizer;
@@ -85,6 +87,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
private final Region mExclusionRegion = Region.obtain();
private final InputManager mInputManager;
private TaskOperations mTaskOperations;
+ private FocusTransitionObserver mFocusTransitionObserver;
/**
* Whether to pilfer the next motion event to send cancellations to the windows below.
@@ -121,7 +124,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
DisplayController displayController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
SyncTransactionQueue syncQueue,
- Transitions transitions) {
+ Transitions transitions,
+ FocusTransitionObserver focusTransitionObserver) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -133,6 +137,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mSyncQueue = syncQueue;
mTransitions = transitions;
+ mFocusTransitionObserver = focusTransitionObserver;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
mTaskOperations = new TaskOperations(null, mContext, mSyncQueue);
}
@@ -148,6 +153,16 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
} catch (RemoteException e) {
Log.e(TAG, "Failed to register window manager callbacks", e);
}
+ mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
+ }
+
+ @Override
+ public void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
+ boolean isFocusedGlobally) {
+ final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
+ if (decor != null) {
+ decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+ }
}
@Override
@@ -180,7 +195,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
return;
}
- decoration.relayout(taskInfo);
+ decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
}
@Override
@@ -217,7 +232,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* setTaskCropAndPosition */);
+ false /* setTaskCropAndPosition */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
}
@@ -230,7 +246,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
if (decoration == null) return;
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* setTaskCropAndPosition */);
+ false /* setTaskCropAndPosition */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
@Override
@@ -308,7 +325,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
windowDecoration.setDragPositioningCallback(taskPositioner);
windowDecoration.setTaskDragResizer(taskPositioner);
windowDecoration.relayout(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */);
+ false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
private class CaptionTouchEventListener implements
@@ -359,7 +377,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
}
if (e.getAction() == MotionEvent.ACTION_DOWN) {
final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
- if (!taskInfo.isFocused) {
+ if (!mFocusTransitionObserver.hasGlobalFocus(taskInfo)) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mTaskToken, true /* onTop */, true /* includingParents */);
mSyncQueue.queue(wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 576c911d4459..509cb85c96cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -174,7 +174,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
}
@Override
- void relayout(RunningTaskInfo taskInfo) {
+ void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
// The crop and position of the task should only be set when a task is fluid resizing. In
// all other cases, it is expected that the transition handler positions and crops the task
@@ -185,7 +185,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
// synced with the buffer transaction (that draws the View). Both will be shown on screen
// at the same, whereas applying them independently causes flickering. See b/270202228.
relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
}
@VisibleForTesting
@@ -196,12 +196,13 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
boolean setTaskCropAndPosition,
boolean isStatusBarVisible,
boolean isKeyguardVisibleAndOccluded,
- InsetsState displayInsetsState) {
+ InsetsState displayInsetsState,
+ boolean hasGlobalFocus) {
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
relayoutParams.mLayoutResId = R.layout.caption_window_decor;
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
- relayoutParams.mShadowRadiusId = taskInfo.isFocused
+ relayoutParams.mShadowRadiusId = hasGlobalFocus
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
@@ -233,7 +234,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
@SuppressLint("MissingPermission")
void relayout(RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
+ boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition,
+ boolean hasGlobalFocus) {
final boolean isFreeform =
taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
final boolean isDragResizeable = ENABLE_WINDOWING_SCALED_RESIZING.isTrue()
@@ -245,7 +247,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
setTaskCropAndPosition, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
- mDisplayController.getInsetsState(taskInfo.displayId));
+ mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus);
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
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 e55bc67ba41b..9e089b2460f6 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
@@ -56,7 +56,6 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -112,6 +111,7 @@ import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -124,6 +124,7 @@ import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
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.extension.InsetsStateKt;
@@ -133,20 +134,21 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
import kotlin.Pair;
import kotlin.Unit;
+import kotlinx.coroutines.ExperimentalCoroutinesApi;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
-import kotlinx.coroutines.ExperimentalCoroutinesApi;
-
/**
* View model for the window decoration with a caption and shadows. Works with
* {@link DesktopModeWindowDecoration}.
*/
-public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
+public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
+ FocusTransitionListener {
private static final String TAG = "DesktopModeWindowDecorViewModel";
private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
@@ -216,6 +218,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
};
private final TaskPositionerFactory mTaskPositionerFactory;
+ private final FocusTransitionObserver mFocusTransitionObserver;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -242,7 +245,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
- Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) {
+ Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
+ FocusTransitionObserver focusTransitionObserver) {
this(
context,
shellExecutor,
@@ -274,7 +278,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
appHandleEducationController,
windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
- new TaskPositionerFactory());
+ new TaskPositionerFactory(),
+ focusTransitionObserver);
}
@VisibleForTesting
@@ -309,7 +314,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
AppHandleEducationController appHandleEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
- TaskPositionerFactory taskPositionerFactory) {
+ TaskPositionerFactory taskPositionerFactory,
+ FocusTransitionObserver focusTransitionObserver) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -369,6 +375,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
};
mTaskPositionerFactory = taskPositionerFactory;
+ mFocusTransitionObserver = focusTransitionObserver;
shellInit.addInitCallback(this::onInit, this);
}
@@ -402,11 +409,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
return Unit.INSTANCE;
});
}
+ mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor);
+ }
+
+ @Override
+ public void onFocusedTaskChanged(int taskId, boolean isFocusedOnDisplay,
+ boolean isFocusedGlobally) {
+ final WindowDecoration decor = mWindowDecorByTaskId.get(taskId);
+ if (decor != null) {
+ decor.relayout(decor.mTaskInfo, isFocusedGlobally);
+ }
}
@Override
public void setFreeformTaskTransitionStarter(FreeformTaskTransitionStarter transitionStarter) {
mTaskOperations = new TaskOperations(transitionStarter, mContext, mSyncQueue);
+ mDesktopTasksController.setFreeformTaskTransitionStarter(transitionStarter);
}
@Override
@@ -447,7 +465,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
removeTaskFromEventReceiver(oldTaskInfo.displayId);
incrementEventReceiverTasks(taskInfo.displayId);
}
- decoration.relayout(taskInfo);
+ decoration.relayout(taskInfo, decoration.mHasGlobalFocus);
mActivityOrientationChangeHandler.ifPresent(handler ->
handler.handleActivityOrientationChange(oldTaskInfo, taskInfo));
}
@@ -486,7 +504,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
createWindowDecoration(taskInfo, taskSurface, startT, finishT);
} else {
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* shouldSetTaskPositionAndCrop */);
+ false /* shouldSetTaskPositionAndCrop */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
}
@@ -499,7 +518,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
if (decoration == null) return;
decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */,
- false /* shouldSetTaskPositionAndCrop */);
+ false /* shouldSetTaskPositionAndCrop */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
}
@Override
@@ -774,11 +794,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
}
} else if (id == R.id.minimize_window) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
- final IBinder transition = mTaskOperations.minimizeTask(mTaskToken, wct);
- mDesktopTasksLimiter.ifPresent(limiter ->
- limiter.addPendingMinimizeChange(transition, mDisplayId, mTaskId));
+ mDesktopTasksController.minimizeTask(decoration.mTaskInfo);
}
}
@@ -895,7 +911,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
private void moveTaskToFront(RunningTaskInfo taskInfo) {
- if (!taskInfo.isFocused) {
+ if (!mFocusTransitionObserver.hasGlobalFocus(taskInfo)) {
mDesktopTasksController.moveTaskToFront(taskInfo);
}
}
@@ -1516,7 +1532,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
windowDecoration.setDragPositioningCallback(taskPositioner);
windowDecoration.relayout(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */);
+ false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */,
+ mFocusTransitionObserver.hasGlobalFocus(taskInfo));
if (!Flags.enableHandleInputFix()) {
incrementEventReceiverTasks(taskInfo.displayId);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index a78fb9b5e245..2c621b1f1a52 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -352,7 +352,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
@Override
- void relayout(ActivityManager.RunningTaskInfo taskInfo) {
+ void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
// The crop and position of the task should only be set when a task is fluid resizing. In
// all other cases, it is expected that the transition handler positions and crops the task
@@ -365,7 +365,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// the View). Both will be shown on screen at the same, whereas applying them independently
// causes flickering. See b/270202228.
final boolean applyTransactionOnDraw = taskInfo.isFreeform();
- relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop);
+ relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop,
+ hasGlobalFocus);
if (!applyTransactionOnDraw) {
t.apply();
}
@@ -373,18 +374,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
Trace.beginSection("DesktopModeWindowDecoration#relayout");
if (taskInfo.isFreeform()) {
// The Task is in Freeform mode -> show its header in sync since it's an integral part
// of the window itself - a delayed header might cause bad UX.
relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
} else {
// The Task is outside Freeform mode -> allow the handle view to be delayed since the
// handle is just a small addition to the window.
relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
}
Trace.endSection();
}
@@ -392,11 +394,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
/** Run the whole relayout phase immediately without delay. */
private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
// Clear the current ViewHost runnable as we will update the ViewHost here
clearCurrentViewHostRunnable();
updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, hasGlobalFocus);
if (mResult.mRootView != null) {
updateViewHost(mRelayoutParams, startT, mResult);
}
@@ -418,7 +421,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
if (applyStartTransactionOnDraw) {
throw new IllegalArgumentException(
"We cannot both sync viewhost ondraw and delay viewhost creation.");
@@ -426,7 +430,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// Clear the current ViewHost runnable as we will update the ViewHost here
clearCurrentViewHostRunnable();
updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT,
- false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop);
+ false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop,
+ hasGlobalFocus);
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
@@ -440,7 +445,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
@SuppressLint("MissingPermission")
private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop,
+ boolean hasGlobalFocus) {
Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
if (Flags.enableDesktopWindowingAppToWeb()) {
@@ -459,7 +465,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
.isTaskInFullImmersiveState(taskInfo.taskId);
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
shouldSetTaskPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
- inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId));
+ inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId),
+ hasGlobalFocus);
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -507,12 +514,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
));
} else {
mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData(
- mTaskInfo, TaskInfoKt.getRequestingImmersive(mTaskInfo), inFullImmersive
+ mTaskInfo, TaskInfoKt.getRequestingImmersive(mTaskInfo), inFullImmersive,
+ hasGlobalFocus
));
}
Trace.endSection();
- if (!mTaskInfo.isFocused) {
+ if (!hasGlobalFocus) {
closeHandleMenu();
closeManageWindowsMenu();
closeMaximizeMenu();
@@ -780,7 +788,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean isStatusBarVisible,
boolean isKeyguardVisibleAndOccluded,
boolean inFullImmersiveMode,
- @NonNull InsetsState displayInsetsState) {
+ @NonNull InsetsState displayInsetsState,
+ boolean hasGlobalFocus) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
final boolean isAppHeader =
captionLayoutId == R.layout.desktop_mode_app_header;
@@ -790,6 +799,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
relayoutParams.mLayoutResId = captionLayoutId;
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
+ relayoutParams.mHasGlobalFocus = hasGlobalFocus;
final boolean showCaption;
if (Flags.enableFullyImmersiveInDesktop()) {
@@ -812,7 +822,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
|| (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
}
relayoutParams.mIsCaptionVisible = showCaption;
-
+ relayoutParams.mIsInsetSource = isAppHeader && !inFullImmersiveMode;
if (isAppHeader) {
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -837,7 +847,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(),
false /* ignoreVisibility */);
relayoutParams.mCaptionTopPadding = systemBarInsets.top;
- relayoutParams.mIsInsetSource = false;
}
// Report occluding elements as bounding rects to the insets system so that apps can
// draw in the empty space in the center:
@@ -865,8 +874,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
relayoutParams.mInputFeatures
|= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
}
- if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) {
- relayoutParams.mShadowRadiusId = taskInfo.isFocused
+ if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ hasGlobalFocus)) {
+ relayoutParams.mShadowRadiusId = hasGlobalFocus
? R.dimen.freeform_decor_shadow_focused_thickness
: R.dimen.freeform_decor_shadow_unfocused_thickness;
}
@@ -1408,7 +1417,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
boolean isFocused() {
- return mTaskInfo.isFocused;
+ return mHasGlobalFocus;
}
/**
@@ -1592,7 +1601,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) {
return windowingMode == WINDOWING_MODE_FULLSCREEN
- ? R.dimen.desktop_mode_fullscreen_decor_caption_height
+ ? com.android.internal.R.dimen.status_bar_height_default
: R.dimen.desktop_mode_freeform_decor_caption_height;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index f4c7fe3eac0c..ccf329c2bb22 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -93,7 +93,7 @@ class FluidResizeTaskPositioner implements TaskPositioner, Transitions.Transitio
mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
mRepositionStartPoint.set(x, y);
mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
- if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mTaskInfo.isFocused) {
+ if (mCtrlType != CTRL_TYPE_UNDEFINED && !mWindowDecoration.mHasGlobalFocus) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mWindowDecoration.mTaskInfo.token, true /* onTop */,
true /* includingParents */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index a1f76d2d1597..ff3b45555bce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -106,7 +106,7 @@ public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.T
// Capture CUJ for re-sizing window in DW mode.
mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface,
mDesktopWindowDecoration.mContext, mHandler, CUJ_DESKTOP_MODE_RESIZE_WINDOW);
- if (!mDesktopWindowDecoration.mTaskInfo.isFocused) {
+ if (!mDesktopWindowDecoration.mHasGlobalFocus) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true /* onTop */,
true /* includingParents */);
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 f8aed412e3fb..ce5cfd0bdc36 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
@@ -125,7 +125,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
}
mDisplayController.removeDisplayWindowListener(this);
- relayout(mTaskInfo);
+ relayout(mTaskInfo, mHasGlobalFocus);
}
};
@@ -146,6 +146,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
boolean mIsStatusBarVisible;
boolean mIsKeyguardVisibleAndOccluded;
+ boolean mHasGlobalFocus;
/** The most recent set of insets applied to this window decoration. */
private WindowDecorationInsets mWindowDecorationInsets;
@@ -199,8 +200,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
*
* @param taskInfo The previous {@link RunningTaskInfo} passed into {@link #relayout} or the
* constructor.
+ * @param hasGlobalFocus Whether the task is focused
*/
- abstract void relayout(RunningTaskInfo taskInfo);
+ abstract void relayout(RunningTaskInfo taskInfo, boolean hasGlobalFocus);
/**
* Used by the {@link DragPositioningCallback} associated with the implementing class to
@@ -225,6 +227,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
if (params.mRunningTaskInfo != null) {
mTaskInfo = params.mRunningTaskInfo;
}
+ mHasGlobalFocus = params.mHasGlobalFocus;
final int oldLayoutResId = mLayoutResId;
mLayoutResId = params.mLayoutResId;
@@ -246,7 +249,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
outResult.mWidth = taskBounds.width();
outResult.mHeight = taskBounds.height();
- outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+ outResult.mRootView.setTaskFocusState(mHasGlobalFocus);
final Resources resources = mDecorWindowContext.getResources();
outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId)
+ params.mCaptionTopPadding;
@@ -391,11 +394,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
final WindowDecorationInsets newInsets = new WindowDecorationInsets(
mTaskInfo.token, mOwner, captionInsetsRect, boundingRects,
- params.mInsetSourceFlags);
+ params.mInsetSourceFlags, params.mIsInsetSource);
if (!newInsets.equals(mWindowDecorationInsets)) {
// Add or update this caption as an insets source.
mWindowDecorationInsets = newInsets;
- mWindowDecorationInsets.addOrUpdate(wct);
+ mWindowDecorationInsets.update(wct);
}
}
@@ -512,7 +515,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mIsKeyguardVisibleAndOccluded = visible && occluded;
final boolean changed = prevVisAndOccluded != mIsKeyguardVisibleAndOccluded;
if (changed) {
- relayout(mTaskInfo);
+ relayout(mTaskInfo, mHasGlobalFocus);
}
}
@@ -522,7 +525,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
final boolean changed = prevStatusBarVisibility != mIsStatusBarVisible;
if (changed) {
- relayout(mTaskInfo);
+ relayout(mTaskInfo, mHasGlobalFocus);
}
}
@@ -710,10 +713,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token,
- mOwner, captionInsets, null /* boundingRets */, 0 /* flags */);
+ mOwner, captionInsets, null /* boundingRets */, 0 /* flags */,
+ true /* shouldAddCaptionInset */);
if (!newInsets.equals(mWindowDecorationInsets)) {
mWindowDecorationInsets = newInsets;
- mWindowDecorationInsets.addOrUpdate(wct);
+ mWindowDecorationInsets.update(wct);
}
}
@@ -737,6 +741,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
boolean mApplyStartTransactionOnDraw;
boolean mSetTaskPositionAndCrop;
+ boolean mHasGlobalFocus;
void reset() {
mLayoutResId = Resources.ID_NULL;
@@ -756,6 +761,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mApplyStartTransactionOnDraw = false;
mSetTaskPositionAndCrop = false;
mWindowDecorConfig = null;
+ mHasGlobalFocus = false;
}
boolean hasInputFeatureSpy() {
@@ -814,21 +820,26 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
private final Rect mFrame;
private final Rect[] mBoundingRects;
private final @InsetsSource.Flags int mFlags;
+ private final boolean mShouldAddCaptionInset;
private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame,
- Rect[] boundingRects, @InsetsSource.Flags int flags) {
+ Rect[] boundingRects, @InsetsSource.Flags int flags,
+ boolean shouldAddCaptionInset) {
mToken = token;
mOwner = owner;
mFrame = frame;
mBoundingRects = boundingRects;
mFlags = flags;
+ mShouldAddCaptionInset = shouldAddCaptionInset;
}
- void addOrUpdate(WindowContainerTransaction wct) {
- wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects,
- mFlags);
- wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
- mBoundingRects, 0 /* flags */);
+ void update(WindowContainerTransaction wct) {
+ if (mShouldAddCaptionInset) {
+ wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects,
+ mFlags);
+ wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
+ mBoundingRects, 0 /* flags */);
+ }
}
void remove(WindowContainerTransaction wct) {
@@ -843,7 +854,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner,
that.mOwner) && Objects.equals(mFrame, that.mFrame)
&& Objects.deepEquals(mBoundingRects, that.mBoundingRects)
- && mFlags == that.mFlags;
+ && mFlags == that.mFlags
+ && mShouldAddCaptionInset == that.mShouldAddCaptionInset;
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index c2af1d45e76f..cf03b3f74dc7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -81,6 +81,7 @@ class AppHeaderViewHolder(
val taskInfo: RunningTaskInfo,
val isRequestingImmersive: Boolean,
val inFullImmersiveState: Boolean,
+ val hasGlobalFocus: Boolean
) : Data()
private val decorThemeUtil = DecorThemeUtil(context)
@@ -159,24 +160,27 @@ class AppHeaderViewHolder(
}
override fun bindData(data: HeaderData) {
- bindData(data.taskInfo, data.isRequestingImmersive, data.inFullImmersiveState)
+ bindData(data.taskInfo, data.isRequestingImmersive, data.inFullImmersiveState,
+ data.hasGlobalFocus)
}
private fun bindData(
taskInfo: RunningTaskInfo,
isRequestingImmersive: Boolean,
inFullImmersiveState: Boolean,
+ hasGlobalFocus: Boolean
) {
if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) {
- bindDataWithThemedHeaders(taskInfo, isRequestingImmersive, inFullImmersiveState)
+ bindDataWithThemedHeaders(taskInfo, isRequestingImmersive, inFullImmersiveState,
+ hasGlobalFocus)
} else {
- bindDataLegacy(taskInfo)
+ bindDataLegacy(taskInfo, hasGlobalFocus)
}
}
- private fun bindDataLegacy(taskInfo: RunningTaskInfo) {
- captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo))
- val color = getAppNameAndButtonColor(taskInfo)
+ private fun bindDataLegacy(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean) {
+ captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo, hasGlobalFocus))
+ val color = getAppNameAndButtonColor(taskInfo, hasGlobalFocus)
val alpha = Color.alpha(color)
closeWindowButton.imageTintList = ColorStateList.valueOf(color)
maximizeWindowButton.imageTintList = ColorStateList.valueOf(color)
@@ -210,9 +214,10 @@ class AppHeaderViewHolder(
private fun bindDataWithThemedHeaders(
taskInfo: RunningTaskInfo,
requestingImmersive: Boolean,
- inFullImmersiveState: Boolean
+ inFullImmersiveState: Boolean,
+ hasGlobalFocus: Boolean
) {
- val header = fillHeaderInfo(taskInfo)
+ val header = fillHeaderInfo(taskInfo, hasGlobalFocus)
val headerStyle = getHeaderStyle(header)
// Caption Background
@@ -455,7 +460,7 @@ class AppHeaderViewHolder(
}
}
- private fun fillHeaderInfo(taskInfo: RunningTaskInfo): Header {
+ private fun fillHeaderInfo(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Header {
return Header(
type = if (taskInfo.isTransparentCaptionBarAppearance) {
Header.Type.CUSTOM
@@ -463,7 +468,7 @@ class AppHeaderViewHolder(
Header.Type.DEFAULT
},
appTheme = decorThemeUtil.getAppTheme(taskInfo),
- isFocused = taskInfo.isFocused,
+ isFocused = hasGlobalFocus,
isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance
)
}
@@ -544,19 +549,19 @@ class AppHeaderViewHolder(
}
@ColorInt
- private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int {
+ private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Int {
if (taskInfo.isTransparentCaptionBarAppearance) {
return Color.TRANSPARENT
}
val materialColorAttr: Int =
if (isDarkMode()) {
- if (!taskInfo.isFocused) {
+ if (!hasGlobalFocus) {
materialColorSurfaceContainerHigh
} else {
materialColorSurfaceDim
}
} else {
- if (!taskInfo.isFocused) {
+ if (!hasGlobalFocus) {
materialColorSurfaceContainerLow
} else {
materialColorSecondaryContainer
@@ -569,7 +574,7 @@ class AppHeaderViewHolder(
}
@ColorInt
- private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int {
+ private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo, hasGlobalFocus: Boolean): Int {
val materialColorAttr = when {
taskInfo.isTransparentCaptionBarAppearance &&
taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer
@@ -579,8 +584,8 @@ class AppHeaderViewHolder(
else -> materialColorOnSecondaryContainer
}
val appDetailsOpacity = when {
- isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY
- !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY
+ isDarkMode() && !hasGlobalFocus -> DARK_THEME_UNFOCUSED_OPACITY
+ !isDarkMode() && !hasGlobalFocus -> LIGHT_THEME_UNFOCUSED_OPACITY
else -> FOCUSED_OPACITY
}
context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) {
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
index 2980d5113bba..e176f47d4094 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.appcompat
import android.platform.test.annotations.Postsubmit
-import android.tools.Rotation
import android.tools.flicker.assertions.FlickerTest
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
@@ -109,9 +108,7 @@ class OpenTransparentActivityTest(flicker: LegacyFlickerTest) : TransparentBaseA
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun getParams(): Collection<FlickerTest> {
- return LegacyFlickerTestFactory.nonRotationTests(
- supportedRotations = listOf(Rotation.ROTATION_90)
- )
+ return LegacyFlickerTestFactory.nonRotationTests()
}
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
index 2484f675b236..9b8c949a1705 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
@@ -20,7 +20,6 @@ import android.graphics.Rect
import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.RequiresDevice
import android.tools.NavBar
-import android.tools.Rotation
import android.tools.flicker.assertions.FlickerTest
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
@@ -266,8 +265,7 @@ class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) : BaseAp
@JvmStatic
fun getParams(): Collection<FlickerTest> {
return LegacyFlickerTestFactory.nonRotationTests(
- supportedNavigationModes = listOf(NavBar.MODE_GESTURAL),
- supportedRotations = listOf(Rotation.ROTATION_90)
+ supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
)
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
index cae609526c65..2e9effb44d67 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -15,23 +15,39 @@
*/
package com.android.wm.shell.desktopmode
+import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS
+import android.os.Binder
import android.os.IBinder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TransitionFlags
+import android.view.WindowManager.TransitionType
+import android.window.TransitionInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -40,14 +56,18 @@ import org.mockito.kotlin.whenever
/**
* Tests for [DesktopFullImmersiveTransitionHandler].
*
- * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandlerTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
@Mock private lateinit var mockTransitions: Transitions
private lateinit var desktopRepository: DesktopRepository
+ @Mock private lateinit var mockDisplayController: DisplayController
+ @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer
private val transactionSupplier = { SurfaceControl.Transaction() }
private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
@@ -57,19 +77,22 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
desktopRepository = DesktopRepository(
context, ShellInit(TestShellExecutor()), mock(), mock()
)
+ whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY))
+ .thenReturn(DisplayLayout())
immersiveHandler = DesktopFullImmersiveTransitionHandler(
transitions = mockTransitions,
desktopRepository = desktopRepository,
- transactionSupplier = transactionSupplier
+ displayController = mockDisplayController,
+ shellTaskOrganizer = mockShellTaskOrganizer,
+ transactionSupplier = transactionSupplier,
)
}
@Test
fun enterImmersive_transitionReady_updatesRepository() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
desktopRepository.setTaskInFullImmersiveState(
displayId = task.displayId,
@@ -77,8 +100,8 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
immersive = false
)
- immersiveHandler.enterImmersive(task, wct)
- immersiveHandler.onTransitionReady(mockBinder)
+ immersiveHandler.moveTaskToImmersive(task)
+ immersiveHandler.onTransitionReady(mockBinder, createTransitionInfo())
assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
}
@@ -86,9 +109,8 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
@Test
fun exitImmersive_transitionReady_updatesRepository() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
desktopRepository.setTaskInFullImmersiveState(
displayId = task.displayId,
@@ -96,8 +118,8 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
immersive = true
)
- immersiveHandler.exitImmersive(task, wct)
- immersiveHandler.onTransitionReady(mockBinder)
+ immersiveHandler.moveTaskToNonImmersive(task)
+ immersiveHandler.onTransitionReady(mockBinder, createTransitionInfo())
assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
}
@@ -105,28 +127,251 @@ class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
@Test
fun enterImmersive_inProgress_ignores() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
- immersiveHandler.enterImmersive(task, wct)
- immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.moveTaskToImmersive(task)
+ immersiveHandler.moveTaskToImmersive(task)
- verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ verify(mockTransitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
}
@Test
fun exitImmersive_inProgress_ignores() {
val task = createFreeformTask()
- val wct = WindowContainerTransaction()
val mockBinder = mock(IBinder::class.java)
- whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
.thenReturn(mockBinder)
- immersiveHandler.exitImmersive(task, wct)
- immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.moveTaskToNonImmersive(task)
+ immersiveHandler.moveTaskToNonImmersive(task)
+
+ verify(mockTransitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_inImmersive_addsPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
- verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isTrue()
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_notInImmersive_doesNotAddPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byDisplay_inImmersive_changesTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(wct.hasBoundsChange(task.token)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byDisplay_notInImmersive_doesNotChangeTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ assertThat(wct.hasBoundsChange(task.token)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_inImmersive_changesTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+
+ assertThat(wct.hasBoundsChange(task.token)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotChangeTaskBounds() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)
+
+ assertThat(wct.hasBoundsChange(task.token)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_inImmersive_addsPendingExitOnRun() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)?.invoke(transition)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotAddPendingExitOnRun() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.exitImmersiveIfApplicable(wct, task.taskId)?.invoke(transition)
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun onTransitionReady_pendingExit_removesPendingExit() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ immersiveHandler.onTransitionReady(
+ transition = transition,
+ info = createTransitionInfo(
+ changes = listOf(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+ )
+ )
+
+ assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+ exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+ && exit.taskId == task.taskId
+ }).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun onTransitionReady_pendingExit_updatesRepository() {
+ val task = createFreeformTask()
+ whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ val wct = WindowContainerTransaction()
+ val transition = Binder()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = DEFAULT_DISPLAY,
+ taskId = task.taskId,
+ immersive = true
+ )
+ immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+ immersiveHandler.onTransitionReady(
+ transition = transition,
+ info = createTransitionInfo(
+ changes = listOf(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+ )
+ )
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+ }
+
+ private fun createTransitionInfo(
+ @TransitionType type: Int = TRANSIT_CHANGE,
+ @TransitionFlags flags: Int = 0,
+ changes: List<TransitionInfo.Change> = emptyList()
+ ): TransitionInfo = TransitionInfo(type, flags).apply {
+ changes.forEach { change -> addChange(change) }
+ }
+
+ private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean =
+ this.changes.any { change ->
+ change.key == token.asBinder()
+ && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index d7a132dfa1be..dde9fda13ea9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -16,7 +16,6 @@
package com.android.wm.shell.desktopmode
-import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
@@ -397,6 +396,37 @@ class DesktopModeEventLoggerTest : ShellTestCase() {
}
}
+ @Test
+ fun logTaskInfoStateInit_logsTaskInfoChangedStateInit() {
+ desktopModeEventLogger.logTaskInfoStateInit()
+ verify {
+ FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+ /* task_event */
+ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD),
+ /* instance_id */
+ eq(0),
+ /* uid */
+ eq(0),
+ /* task_height */
+ eq(0),
+ /* task_width */
+ eq(0),
+ /* task_x */
+ eq(0),
+ /* task_y */
+ eq(0),
+ /* session_id */
+ eq(0),
+ /* minimize_reason */
+ eq(UNSET_MINIMIZE_REASON),
+ /* unminimize_reason */
+ eq(UNSET_UNMINIMIZE_REASON),
+ /* visible_task_count */
+ eq(0)
+ )
+ }
+ }
+
private companion object {
private const val SESSION_ID = 1
private const val TASK_ID = 1
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index daf7e7d5397b..e7593b5b9324 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -115,6 +115,9 @@ class DesktopModeLoggerTransitionObserverTest : ShellTestCase() {
val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
initRunnableCaptor.value.run()
+ // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a
+ // consistent state with no outstanding interactions when test cases start executing.
+ verify(desktopModeEventLogger).logTaskInfoStateInit()
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 1308114febbc..e20f0ecb1f3b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -957,6 +957,15 @@ class DesktopRepositoryTest : ShellTestCase() {
assertThat(repo.getActiveTasks(displayId = DEFAULT_DISPLAY)).isEmpty()
}
+ @Test
+ fun getTaskInFullImmersiveState_byDisplay() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+
+ assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID)).isEqualTo(1)
+ assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2)
+ }
+
class TestListener : DesktopRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
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 27deb0b6abf6..b3c10d64c3a3 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
@@ -42,6 +42,7 @@ import android.graphics.Rect
import android.os.Binder
import android.os.Bundle
import android.os.Handler
+import android.os.IBinder
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
@@ -99,6 +100,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplit
import com.android.wm.shell.desktopmode.persistence.Desktop
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository
import com.android.wm.shell.draganddrop.DragAndDropController
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.recents.RecentTasksController
import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
@@ -144,13 +146,11 @@ import org.mockito.Mockito
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
-import org.mockito.kotlin.argThat
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
@@ -201,6 +201,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
@Mock private lateinit var mockSurface: SurfaceControl
@Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener
+ @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
@Mock private lateinit var mockHandler: Handler
@Mock lateinit var persistentRepository: DesktopPersistentRepository
@@ -266,6 +267,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller = createController()
controller.setSplitScreenController(splitScreenController)
+ controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter
shellInit.init()
@@ -1542,75 +1544,142 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun onDesktopWindowMinimize_noActiveTask_doesntUpdateTransaction() {
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = 1)
- // Nothing happens.
- assertThat(wct.hierarchyOps).isEmpty()
+ fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() {
+ val task = setUpFreeformTask(active = false)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+ val wallpaperToken = MockToken().token()
+ taskRepository.wallpaperActivityToken = wallpaperToken
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
- fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntUpdateTransaction() {
- val task = setUpFreeformTask()
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
- // Nothing happens.
- assertThat(wct.hierarchyOps).isEmpty()
+ fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
+ val task = setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK
+ }
}
@Test
fun onDesktopWindowMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() {
val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
- val wct = WindowContainerTransaction()
// The only active task is being minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
// Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
+ captor.value.assertRemoveAt(index = 0, wallpaperToken)
}
@Test
- fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntUpdateTransaction() {
+ fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() {
val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
- val wct = WindowContainerTransaction()
// The only active task is already minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
+ controller.minimizeTask(task)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
- fun onDesktopWindowMinimize_multipleActiveTasks_doesntUpdateTransaction() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
+ fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() {
+ val task1 = setUpFreeformTask(active = true)
+ setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowMinimize(wct, taskId = task1.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
+ controller.minimizeTask(task1)
+
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ captor.value.hierarchyOps.none { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder()
+ }
}
@Test
fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
+ val task1 = setUpFreeformTask(active = true)
+ val task2 = setUpFreeformTask(active = true)
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
val wallpaperToken = MockToken().token()
taskRepository.wallpaperActivityToken = wallpaperToken
taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
- val wct = WindowContainerTransaction()
// task1 is the only visible task as task2 is minimized.
- controller.onDesktopWindowMinimize(wct, taskId = task1.taskId)
+ controller.minimizeTask(task1)
// Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
+ val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture())
+ // Adds remove wallpaper operation
+ captor.value.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_triesToExitImmersive() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+
+ controller.minimizeTask(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(any(), eq(task))
+ }
+
+ @Test
+ fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ val runOnTransit = RunOnStartTransitionCallback()
+ whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
+ .thenReturn(transition)
+ whenever(mockDesktopFullImmersiveTransitionHandler.exitImmersiveIfApplicable(any(), eq(task)))
+ .thenReturn(runOnTransit)
+
+ controller.minimizeTask(task)
+
+ assertThat(runOnTransit.invocations).isEqualTo(1)
+ assertThat(runOnTransit.lastInvoked).isEqualTo(transition)
}
@Test
@@ -3166,27 +3235,23 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun toggleImmersive_enter_resizesToDisplayBounds() {
+ fun toggleImmersive_enter_movesToImmersive() {
val task = setUpFreeformTask(DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
controller.toggleDesktopTaskFullImmersiveState(task)
- verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
- wct.hasBoundsChange(task.token, Rect())
- })
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToImmersive(task)
}
@Test
- fun toggleImmersive_exit_resizesToStableBounds() {
+ fun toggleImmersive_exit_movesToNonImmersive() {
val task = setUpFreeformTask(DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
controller.toggleDesktopTaskFullImmersiveState(task)
- verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
- wct.hasBoundsChange(task.token, STABLE_BOUNDS)
- })
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
}
@Test
@@ -3198,7 +3263,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
task.requestedVisibleTypes = WindowInsets.Type.statusBars()
controller.onTaskInfoChanged(task)
- verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), any())
+ verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
}
@Test
@@ -3210,7 +3275,113 @@ class DesktopTasksControllerTest : ShellTestCase() {
task.requestedVisibleTypes = WindowInsets.Type.statusBars()
controller.onTaskInfoChanged(task)
- verify(mockDesktopFullImmersiveTransitionHandler, never()).exitImmersive(eq(task), any())
+ verify(mockDesktopFullImmersiveTransitionHandler, never()).moveTaskToNonImmersive(task)
+ }
+
+ @Test
+ fun moveTaskToDesktop_background_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = true)
+ val wct = WindowContainerTransaction()
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+ whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+ controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToDesktop_foreground_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = false)
+ val wct = WindowContainerTransaction()
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(wct, task.displayId)).thenReturn(runOnStartTransit)
+ whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
+
+ controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(wct, task.displayId)
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToFront_background_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = true)
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.moveTaskToFront(task.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun moveTaskToFront_foreground_attemptsImmersiveExit() {
+ val task = setUpFreeformTask(background = false)
+ val runOnStartTransit = RunOnStartTransitionCallback()
+ val transition = Binder()
+ whenever(mockDesktopFullImmersiveTransitionHandler
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit)
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.moveTaskToFront(task.taskId)
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(any(), eq(task.displayId))
+ runOnStartTransit.assertOnlyInvocation(transition)
+ }
+
+ @Test
+ fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() {
+ markTaskVisible(setUpFreeformTask())
+ val task = setUpFreeformTask()
+ markTaskVisible(task)
+ val binder = Binder()
+
+ controller.handleRequest(binder, createTransition(task))
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+ }
+
+ @Test
+ fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() {
+ setUpFreeformTask()
+ val task = setUpFullscreenTask()
+ val binder = Binder()
+
+ controller.handleRequest(binder, createTransition(task))
+
+ verify(mockDesktopFullImmersiveTransitionHandler)
+ .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
+ }
+
+ private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
+ var invocations = 0
+ private set
+ var lastInvoked: IBinder? = null
+ private set
+
+ override fun invoke(transition: IBinder) {
+ invocations++
+ lastInvoked = transition
+ }
+ }
+
+ private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) {
+ assertThat(invocations).isEqualTo(1)
+ assertThat(lastInvoked).isEqualTo(transition)
}
/**
@@ -3291,18 +3462,27 @@ class DesktopTasksControllerTest : ShellTestCase() {
private fun setUpFreeformTask(
displayId: Int = DEFAULT_DISPLAY,
bounds: Rect? = null,
- active: Boolean = true
+ active: Boolean = true,
+ background: Boolean = false,
): RunningTaskInfo {
val task = createFreeformTask(displayId, bounds)
val activityInfo = ActivityInfo()
task.topActivityInfo = activityInfo
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ if (background) {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+ whenever(recentTasksController.findTaskInBackground(task.taskId))
+ .thenReturn(createTaskInfo(task.taskId))
+ } else {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ }
if (active) {
taskRepository.addActiveTask(displayId, task.taskId)
taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true)
}
taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
- runningTasks.add(task)
+ if (!background) {
+ runningTasks.add(task)
+ }
return task
}
@@ -3556,6 +3736,21 @@ private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowC
assertThat(op.container).isEqualTo(token.asBinder())
}
+private fun WindowContainerTransaction.assertNoRemoveAt(index: Int, token: WindowContainerToken) {
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+ assertThat(op.container).isEqualTo(token.asBinder())
+}
+
+private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowContainerToken) {
+
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+ assertThat(op.container).isEqualTo(token.asBinder())
+}
+
private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) {
assertIndexInBounds(index)
val op = hierarchyOps[index]
@@ -3578,13 +3773,6 @@ private fun WindowContainerTransaction.assertLaunchTaskAt(
.isEqualTo(windowingMode)
}
-private fun WindowContainerTransaction.hasBoundsChange(
- token: WindowContainerToken,
- bounds: Rect
-): Boolean = this.changes.any { change ->
- change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
-}
-
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index 598df34a310d..fe87aa88a8db 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -22,26 +22,38 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.os.IBinder
import android.platform.test.annotations.EnableFlags
import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.IWindowContainerToken
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.WindowContainerToken
+import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK
import com.android.modules.utils.testing.ExtendedMockitoRule
import com.android.window.flags.Flags
+import com.android.wm.shell.MockToken
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isA
import org.mockito.Mockito
import org.mockito.kotlin.any
+import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.spy
@@ -130,6 +142,27 @@ class DesktopTasksTransitionObserverTest {
verify(taskRepository).removeFreeformTask(task.displayId, task.taskId)
}
+ @Test
+ fun closeLastTask_wallpaperTokenExists_wallpaperIsRemoved() {
+ val mockTransition = Mockito.mock(IBinder::class.java)
+ val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+ val wallpaperToken = MockToken().token()
+ whenever(taskRepository.getVisibleTaskCount(task.displayId)).thenReturn(1)
+ whenever(taskRepository.wallpaperActivityToken).thenReturn(wallpaperToken)
+
+ transitionObserver.onTransitionReady(
+ transition = mockTransition,
+ info = createCloseTransition(task),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+ transitionObserver.onTransitionFinished(mockTransition, false)
+
+ val wct = getLatestWct(type = TRANSIT_CLOSE)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ wct.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
private fun createBackNavigationTransition(
task: RunningTaskInfo?
): TransitionInfo {
@@ -160,6 +193,48 @@ class DesktopTasksTransitionObserverTest {
}
}
+ private fun createCloseTransition(
+ task: RunningTaskInfo?
+ ): TransitionInfo {
+ return TransitionInfo(TRANSIT_CLOSE, 0 /* flags */).apply {
+ addChange(
+ Change(mock(), mock()).apply {
+ mode = TRANSIT_CLOSE
+ parent = null
+ taskInfo = task
+ flags = flags
+ }
+ )
+ }
+ }
+
+ private fun getLatestWct(
+ @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
+ handlerClass: Class<out Transitions.TransitionHandler>? = null
+ ): WindowContainerTransaction {
+ val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (handlerClass == null) {
+ Mockito.verify(transitions).startTransition(eq(type), arg.capture(), isNull())
+ } else {
+ Mockito.verify(transitions)
+ .startTransition(eq(type), arg.capture(), isA(handlerClass))
+ }
+ return arg.value
+ }
+
+ private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) {
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+ assertThat(op.container).isEqualTo(token.asBinder())
+ }
+
+ private fun WindowContainerTransaction.assertIndexInBounds(index: Int) {
+ assertWithMessage("WCT does not have a hierarchy operation at index $index")
+ .that(hierarchyOps.size)
+ .isGreaterThan(index)
+ }
+
private fun createTaskInfo(id: Int, windowingMode: Int = WINDOWING_MODE_FREEFORM) =
RunningTaskInfo().apply {
taskId = id
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 36e0427a7e22..f95b0d1e7287 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -178,6 +178,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
mFreeformTaskListener.onTaskVanished(task);
verify(mDesktopRepository, never()).minimizeTask(task.displayId, task.taskId);
+ verify(mDesktopRepository).removeClosingTask(task.taskId);
verify(mDesktopRepository).removeFreeformTask(task.displayId, task.taskId);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 145819f3f727..7ae0bcd13681 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -329,7 +329,7 @@ public class FreeformTaskTransitionObserverTest {
mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
- verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+ verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition, info);
}
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index 0f16b9d0fa7e..5ebf5170bf86 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -49,7 +49,8 @@ class CaptionWindowDecorationTests : ShellTestCase() {
false,
true /* isStatusBarVisible */,
false /* isKeyguardVisibleAndOccluded */,
- InsetsState()
+ InsetsState(),
+ true /* hasGlobalFocus */
)
Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue()
@@ -70,7 +71,8 @@ class CaptionWindowDecorationTests : ShellTestCase() {
false,
true /* isStatusBarVisible */,
false /* isKeyguardVisibleAndOccluded */,
- InsetsState()
+ InsetsState(),
+ true /* hasGlobalFocus */
)
Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse()
@@ -87,7 +89,8 @@ class CaptionWindowDecorationTests : ShellTestCase() {
false,
true /* isStatusBarVisible */,
false /* isKeyguardVisibleAndOccluded */,
- InsetsState()
+ InsetsState(),
+ true /* hasGlobalFocus */
)
Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo(
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 4aa7e18b4b84..175fbd2396e3 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
@@ -100,6 +100,7 @@ import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
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.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
@@ -126,7 +127,6 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.any
-import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argThat
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
@@ -192,6 +192,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
DesktopModeWindowDecorViewModel.TaskPositionerFactory
@Mock private lateinit var mockTaskPositioner: TaskPositioner
@Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController
+ @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
@Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
private lateinit var spyContext: TestableContext
@@ -254,7 +255,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
mockAppHandleEducationController,
mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
- mockTaskPositionerFactory
+ mockTaskPositionerFactory,
+ mockFocusTransitionObserver
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -455,24 +457,13 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
onClickListenerCaptor.value.onClick(view)
- val transactionCaptor = argumentCaptor<WindowContainerTransaction>()
- verify(mockFreeformTaskTransitionStarter)
- .startMinimizedModeTransition(transactionCaptor.capture())
- val wct = transactionCaptor.firstValue
-
- verify(mockTasksLimiter).addPendingMinimizeChange(
- anyOrNull(), eq(DEFAULT_DISPLAY), eq(decor.mTaskInfo.taskId))
-
- assertEquals(1, wct.getHierarchyOps().size)
- assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REORDER, wct.getHierarchyOps().get(0).getType())
- assertFalse(wct.getHierarchyOps().get(0).getToTop())
- assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
+ verify(mockDesktopTasksController).minimizeTask(decor.mTaskInfo)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() {
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply {
isTopActivityTransparent = true
isTopActivityStyleFloating = true
numActivities = 1
@@ -487,7 +478,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsNotCreatedForTopTranslucentActivitiesWithoutStyleFloating() {
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply {
isTopActivityTransparent = true
isTopActivityStyleFloating = false
numActivities = 1
@@ -500,7 +491,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsNotCreatedForSystemUIActivities() {
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
// Set task as systemUI package
val systemUIPackageName = context.resources.getString(
@@ -573,7 +564,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
// Simulate default enforce device restrictions system property
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
// Simulate device that doesn't support desktop mode
doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
@@ -589,7 +580,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
// Simulate device that doesn't support desktop mode
doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
setUpMockDecorationsForTasks(task)
onTaskOpening(task)
@@ -602,7 +593,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
// Simulate default enforce device restrictions system property
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
setUpMockDecorationsForTasks(task)
@@ -1045,7 +1036,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask =
createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask =
@@ -1073,7 +1064,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
fun testOnDisplayRotation_taskInValidArea_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask =
createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask =
@@ -1100,7 +1091,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
fun testOnDisplayRotation_sameOrientationRotation_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask =
createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask =
@@ -1124,7 +1115,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
fun testOnDisplayRotation_differentDisplayId_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FREEFORM)
val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_FREEFORM)
@@ -1149,7 +1140,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Test
fun testOnDisplayRotation_nonFreeformTask_taskBoundsNotUpdated() {
- val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM)
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FULLSCREEN)
val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_PINNED)
@@ -1322,7 +1313,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
displayId: Int = DEFAULT_DISPLAY,
@WindowingMode windowingMode: Int,
activityType: Int = ACTIVITY_TYPE_STANDARD,
- focused: Boolean = true,
activityInfo: ActivityInfo = ActivityInfo(),
requestingImmersive: Boolean = false
): RunningTaskInfo {
@@ -1333,7 +1323,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
.setActivityType(activityType)
.build().apply {
topActivityInfo = activityInfo
- isFocused = focused
isResizeable = true
requestedVisibleTypes = if (requestingImmersive) {
statusBars().inv()
@@ -1351,7 +1340,6 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
any(), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
- whenever(decoration.isFocused).thenReturn(task.isFocused)
whenever(decoration.user).thenReturn(mockUserHandle)
if (task.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 35be80e87ecd..1d11d2e8ff06 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -279,7 +279,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration spyWindowDecor =
spy(createWindowDecoration(taskInfo));
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
// Menus should close if open before the task being invisible causes relayout to return.
verify(spyWindowDecor).closeHandleMenu();
@@ -298,7 +298,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
}
@@ -318,7 +319,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
}
@@ -343,7 +345,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
}
@@ -369,7 +372,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
}
@@ -391,7 +395,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
}
@@ -412,7 +417,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -432,7 +438,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -452,7 +459,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
}
@@ -473,7 +481,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -494,7 +503,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -516,7 +526,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();
}
@@ -539,7 +550,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue();
}
@@ -560,7 +572,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0)
@@ -583,7 +596,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0)
@@ -612,7 +626,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- insetsState);
+ insetsState,
+ /* hasGlobalFocus= */ true);
// Takes status bar inset as padding, ignores caption bar inset.
assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
@@ -634,7 +649,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsInsetSource).isFalse();
}
@@ -655,7 +671,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ false,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
// Header is always shown because it's assumed the status bar is always visible.
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -676,7 +693,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
}
@@ -696,7 +714,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ false,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -716,7 +735,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ true,
/* inFullImmersiveMode */ false,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -737,7 +757,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isTrue();
@@ -750,7 +771,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ false,
/* isKeyguardVisibleAndOccluded */ false,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -771,7 +793,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* isStatusBarVisible */ true,
/* isKeyguardVisibleAndOccluded */ true,
/* inFullImmersiveMode */ true,
- new InsetsState());
+ new InsetsState(),
+ /* hasGlobalFocus= */ true);
assertThat(relayoutParams.mIsCaptionVisible).isFalse();
}
@@ -782,7 +805,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockTransaction).apply();
verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any());
@@ -797,7 +820,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
// Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
taskInfo.isResizeable = false;
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockTransaction, never()).apply();
verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction);
@@ -809,7 +832,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
}
@@ -821,7 +844,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Once for view host, the other for the AppHandle input layer.
verify(mMockHandler, times(2)).post(runnableArgument.capture());
@@ -838,7 +861,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
// Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
taskInfo.isResizeable = false;
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
verify(mMockHandler, never()).post(any());
@@ -850,11 +873,11 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Once for view host, the other for the AppHandle input layer.
verify(mMockHandler, times(2)).post(runnableArgument.capture());
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
}
@@ -865,7 +888,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Once for view host, the other for the AppHandle input layer.
verify(mMockHandler, times(2)).post(runnableArgument.capture());
@@ -998,7 +1021,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
runnableArgument.getValue().run();
// Relayout decor with same captured link
- decor.relayout(taskInfo);
+ decor.relayout(taskInfo, true /* hasGlobalFocus */);
// Verify handle menu's browser link not set to captured link since link is expired
createHandleMenu(decor);
@@ -1147,7 +1170,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockCaptionHandleRepository, never()).notifyCaptionChanged(any());
}
@@ -1164,7 +1187,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
captionStateArgumentCaptor.capture());
@@ -1191,7 +1214,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockAppHeaderViewHolder, atLeastOnce()).runOnAppChipGlobalLayout(
runnableArgumentCaptor.capture());
runnableArgumentCaptor.getValue().invoke();
@@ -1214,7 +1237,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
captionStateArgumentCaptor.capture());
@@ -1234,7 +1257,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
createHandleMenu(spyWindowDecor);
verify(mMockCaptionHandleRepository, atLeastOnce()).notifyCaptionChanged(
@@ -1259,7 +1282,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
ArgumentCaptor<CaptionState> captionStateArgumentCaptor = ArgumentCaptor.forClass(
CaptionState.class);
- spyWindowDecor.relayout(taskInfo);
+ spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
createHandleMenu(spyWindowDecor);
spyWindowDecor.closeHandleMenu();
@@ -1356,7 +1379,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
windowDecor.setOpenInBrowserClickListener(mMockOpenInBrowserClickListener);
windowDecor.mDecorWindowContext = mContext;
if (relayout) {
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
}
return windowDecor;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 7543fed4b085..ca1f9abed09e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -624,7 +624,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
@Test
fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() {
- mockWindowDecoration.mTaskInfo.isFocused = false
+ mockWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -640,7 +640,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
@Test
fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() {
- mockWindowDecoration.mTaskInfo.isFocused = true
+ mockWindowDecoration.mHasGlobalFocus = true
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -656,7 +656,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
@Test
fun testDragResize_drag_draggedTaskNotReorderedToTop() {
- mockWindowDecoration.mTaskInfo.isFocused = false
+ mockWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
STARTING_BOUNDS.left.toFloat(),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 1273ee823159..1dfbd6705bf4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -323,7 +323,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
@Test
fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread {
- mockDesktopWindowDecoration.mTaskInfo.isFocused = false
+ mockDesktopWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -339,7 +339,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
@Test
fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread {
- mockDesktopWindowDecoration.mTaskInfo.isFocused = true
+ mockDesktopWindowDecoration.mHasGlobalFocus = true
taskPositioner.onDragPositioningStart(
CTRL_TYPE_RIGHT, // Resize right
STARTING_BOUNDS.left.toFloat(),
@@ -355,7 +355,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
@Test
fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread {
- mockDesktopWindowDecoration.mTaskInfo.isFocused = false
+ mockDesktopWindowDecoration.mHasGlobalFocus = false
taskPositioner.onDragPositioningStart(
CTRL_TYPE_UNDEFINED, // drag
STARTING_BOUNDS.left.toFloat(),
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 54dd15baa4c0..bb41e9c81ece 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
@@ -203,13 +203,12 @@ public class WindowDecorationTests extends ShellTestCase {
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(false)
.build();
- taskInfo.isFocused = false;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
verify(decorContainerSurfaceBuilder, never()).build();
verify(taskBackgroundSurfaceBuilder, never()).build();
@@ -243,13 +242,12 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(decorContainerSurfaceBuilder).setParent(mMockTaskSurface);
verify(decorContainerSurfaceBuilder).setContainerLayer();
@@ -316,14 +314,13 @@ public class WindowDecorationTests extends ShellTestCase {
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlViewHost, never()).release();
verify(t, never()).apply();
@@ -333,7 +330,7 @@ public class WindowDecorationTests extends ShellTestCase {
final SurfaceControl.Transaction t2 = mock(SurfaceControl.Transaction.class);
mMockSurfaceControlTransactions.add(t2);
taskInfo.isVisible = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, false /* hasGlobalFocus */);
final InOrder releaseOrder = inOrder(t2, mMockSurfaceControlViewHost);
releaseOrder.verify(mMockSurfaceControlViewHost).release();
@@ -361,7 +358,7 @@ public class WindowDecorationTests extends ShellTestCase {
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// It shouldn't show the window decoration when it can't obtain the display instance.
assertThat(mRelayoutResult.mRootView).isNull();
@@ -417,10 +414,9 @@ public class WindowDecorationTests extends ShellTestCase {
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
final SurfaceControl additionalWindowSurface = mock(SurfaceControl.class);
final SurfaceControl.Builder additionalWindowSurfaceBuilder =
@@ -470,11 +466,10 @@ public class WindowDecorationTests extends ShellTestCase {
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface);
verify(captionContainerSurfaceBuilder).setContainerLayer();
@@ -510,11 +505,11 @@ public class WindowDecorationTests extends ShellTestCase {
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
.build();
- taskInfo.isFocused = true;
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */);
+ windowDecor.relayout(taskInfo, true /* applyStartTransactionOnDraw */,
+ true /* hasGlobalFocus */);
verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT);
}
@@ -549,10 +544,9 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).setColor(mMockTaskSurface, new float[]{1.f, 1.f, 0.f});
@@ -575,7 +569,7 @@ public class WindowDecorationTests extends ShellTestCase {
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()), any(), any(), anyInt());
@@ -611,10 +605,9 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
.build();
- taskInfo.isFocused = true;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).unsetColor(mMockTaskSurface);
@@ -635,7 +628,7 @@ public class WindowDecorationTests extends ShellTestCase {
// Hidden from the beginning, so no insets were ever added.
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsCaptionVisible = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Never added.
verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
@@ -663,7 +656,7 @@ public class WindowDecorationTests extends ShellTestCase {
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mIsInsetSource = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Never added.
verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
@@ -687,11 +680,11 @@ public class WindowDecorationTests extends ShellTestCase {
mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mIsInsetSource = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mIsInsetSource = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Insets should be removed.
verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
@@ -715,7 +708,7 @@ public class WindowDecorationTests extends ShellTestCase {
// Relayout will add insets.
mRelayoutParams.mIsCaptionVisible = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()), any(), any(), anyInt());
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
@@ -768,10 +761,10 @@ public class WindowDecorationTests extends ShellTestCase {
final ActivityManager.RunningTaskInfo firstTaskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
- windowDecor.relayout(firstTaskInfo);
+ windowDecor.relayout(firstTaskInfo, true /* hasGlobalFocus */);
final ActivityManager.RunningTaskInfo secondTaskInfo =
builder.setToken(token).setBounds(new Rect(50, 50, 1000, 1000)).build();
- windowDecor.relayout(secondTaskInfo);
+ windowDecor.relayout(secondTaskInfo, true /* hasGlobalFocus */);
// Insets should be applied twice.
verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(),
@@ -796,10 +789,10 @@ public class WindowDecorationTests extends ShellTestCase {
final ActivityManager.RunningTaskInfo firstTaskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
- windowDecor.relayout(firstTaskInfo);
+ windowDecor.relayout(firstTaskInfo, true /* hasGlobalFocus */);
final ActivityManager.RunningTaskInfo secondTaskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
- windowDecor.relayout(secondTaskInfo);
+ windowDecor.relayout(secondTaskInfo, true /* hasGlobalFocus */);
// Insets should only need to be applied once.
verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(),
@@ -824,7 +817,7 @@ public class WindowDecorationTests extends ShellTestCase {
mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mInsetSourceFlags =
FLAG_FORCE_CONSUMING | FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
// Caption inset source should add params' flags.
verify(mMockWindowContainerTransaction).addInsetsSource(eq(token), any(),
@@ -845,14 +838,13 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mSetTaskPositionAndCrop = false;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT, never()).setWindowCrop(
eq(mMockTaskSurface), anyInt(), anyInt());
@@ -875,13 +867,12 @@ public class WindowDecorationTests extends ShellTestCase {
.setVisible(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.build();
- taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
mRelayoutParams.mSetTaskPositionAndCrop = true;
- windowDecor.relayout(taskInfo);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */);
verify(mMockSurfaceControlStartT).setWindowCrop(
eq(mMockTaskSurface), anyInt(), anyInt());
@@ -932,12 +923,12 @@ public class WindowDecorationTests extends ShellTestCase {
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
assertTrue(decor.mIsStatusBarVisible);
decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
- verify(decor, times(2)).relayout(task);
+ verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
}
@Test
@@ -947,11 +938,11 @@ public class WindowDecorationTests extends ShellTestCase {
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */));
- verify(decor, times(1)).relayout(task);
+ verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
}
@Test
@@ -960,13 +951,13 @@ public class WindowDecorationTests extends ShellTestCase {
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
assertFalse(decor.mIsKeyguardVisibleAndOccluded);
decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
assertTrue(decor.mIsKeyguardVisibleAndOccluded);
- verify(decor, times(2)).relayout(task);
+ verify(decor, times(2)).relayout(task, true /* hasGlobalFocus */);
}
@Test
@@ -975,19 +966,18 @@ public class WindowDecorationTests extends ShellTestCase {
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
final TestWindowDecoration decor = spy(createWindowDecoration(task));
- decor.relayout(task);
+ decor.relayout(task, true /* hasGlobalFocus */);
assertFalse(decor.mIsKeyguardVisibleAndOccluded);
decor.onKeyguardStateChanged(false /* visible */, true /* occluding */);
- verify(decor, times(1)).relayout(task);
+ verify(decor, times(1)).relayout(task, true /* hasGlobalFocus */);
}
private ActivityManager.RunningTaskInfo createTaskInfo() {
final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
.setVisible(true)
.build();
- taskInfo.isFocused = true;
return taskInfo;
}
@@ -1055,8 +1045,8 @@ public class WindowDecorationTests extends ShellTestCase {
}
@Override
- void relayout(ActivityManager.RunningTaskInfo taskInfo) {
- relayout(taskInfo, false /* applyStartTransactionOnDraw */);
+ void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) {
+ relayout(taskInfo, false /* applyStartTransactionOnDraw */, hasGlobalFocus);
}
@Override
@@ -1078,10 +1068,11 @@ public class WindowDecorationTests extends ShellTestCase {
}
void relayout(ActivityManager.RunningTaskInfo taskInfo,
- boolean applyStartTransactionOnDraw) {
+ boolean applyStartTransactionOnDraw, boolean hasGlobalFocus) {
mRelayoutParams.mRunningTaskInfo = taskInfo;
mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
mRelayoutParams.mLayoutResId = R.layout.caption_layout;
+ mRelayoutParams.mHasGlobalFocus = hasGlobalFocus;
relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
mMockWindowContainerTransaction, mMockView, mRelayoutResult);
}
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index bc269fedddfe..e9845c1d9f13 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -15,7 +15,8 @@ package com.google.android.appfunctions.sidecar {
public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
index 6e91de6bbcf2..2a168e871713 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -24,8 +24,8 @@ import android.annotation.Nullable;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
-import android.os.IBinder;
import android.os.CancellationSignal;
+import android.os.IBinder;
import android.util.Log;
import java.util.function.Consumer;
@@ -71,18 +71,21 @@ public abstract class AppFunctionService extends Service {
private final Binder mBinder =
android.app.appfunctions.AppFunctionService.createBinder(
/* context= */ this,
- /* onExecuteFunction= */ (platformRequest, cancellationSignal, callback) -> {
+ /* onExecuteFunction= */ (platformRequest,
+ callingPackage,
+ cancellationSignal,
+ callback) -> {
AppFunctionService.this.onExecuteFunction(
SidecarConverter.getSidecarExecuteAppFunctionRequest(
platformRequest),
+ callingPackage,
cancellationSignal,
(sidecarResponse) -> {
callback.accept(
SidecarConverter.getPlatformExecuteAppFunctionResponse(
sidecarResponse));
});
- }
- );
+ });
@NonNull
@Override
@@ -107,11 +110,49 @@ public abstract class AppFunctionService extends Service {
* thread and dispatch the result with the given callback. You should always report back the
* result using the callback, no matter if the execution was successful or not.
*
+ * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel
+ * the execution of function if requested by the system.
+ *
+ * @param request The function execution request.
+ * @param callingPackage The package name of the app that is requesting the execution.
+ * @param cancellationSignal A signal to cancel the execution.
+ * @param callback A callback to report back the result.
+ */
+ @MainThread
+ public void onExecuteFunction(
+ @NonNull ExecuteAppFunctionRequest request,
+ @NonNull String callingPackage,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ onExecuteFunction(request, cancellationSignal, callback);
+ }
+
+ /**
+ * Called by the system to execute a specific app function.
+ *
+ * <p>This method is triggered when the system requests your AppFunctionService to handle a
+ * particular function you have registered and made available.
+ *
+ * <p>To ensure proper routing of function requests, assign a unique identifier to each
+ * function. This identifier doesn't need to be globally unique, but it must be unique within
+ * your app. For example, a function to order food could be identified as "orderFood". In most
+ * cases this identifier should come from the ID automatically generated by the AppFunctions
+ * SDK. You can determine the specific function to invoke by calling {@link
+ * ExecuteAppFunctionRequest#getFunctionIdentifier()}.
+ *
+ * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker
+ * thread and dispatch the result with the given callback. You should always report back the
+ * result using the callback, no matter if the execution was successful or not.
+ *
* @param request The function execution request.
* @param cancellationSignal A {@link CancellationSignal} to cancel the request.
* @param callback A callback to report back the result.
+ * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String,
+ * CancellationSignal, Consumer)} instead. This method will be removed once usage references
+ * are updated.
*/
@MainThread
+ @Deprecated
public void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
@NonNull CancellationSignal cancellationSignal,
@@ -138,7 +179,6 @@ public abstract class AppFunctionService extends Service {
*
* @param request The function execution request.
* @param callback A callback to report back the result.
- *
* @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal,
* Consumer)} instead. This method will be removed once usage references are updated.
*/
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
index d87fec7985e9..969e5d58b909 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -234,12 +234,13 @@ public final class ExecuteAppFunctionResponse {
@IntDef(
prefix = {"RESULT_"},
value = {
- RESULT_OK,
- RESULT_DENIED,
- RESULT_APP_UNKNOWN_ERROR,
- RESULT_INTERNAL_ERROR,
- RESULT_INVALID_ARGUMENT,
- RESULT_DISABLED
+ RESULT_OK,
+ RESULT_DENIED,
+ RESULT_APP_UNKNOWN_ERROR,
+ RESULT_INTERNAL_ERROR,
+ RESULT_INVALID_ARGUMENT,
+ RESULT_DISABLED,
+ RESULT_CANCELLED
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResultCode {}
diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java
index 80b606c9ec9e..5e55f64da985 100644
--- a/media/java/android/media/MediaMuxer.java
+++ b/media/java/android/media/MediaMuxer.java
@@ -34,7 +34,7 @@ import java.util.Map;
/**
* MediaMuxer facilitates muxing elementary streams. Currently MediaMuxer supports MP4, Webm
- * and 3GP file as the output. It also supports muxing B-frames in MP4 since Android Nougat.
+ * and 3GP file as the output. It also supports muxing B-frames in MP4 since Android Nougat MR1.
* <p>
* It is generally used like this:
*
@@ -191,14 +191,14 @@ import java.util.Map;
<td>&#9675;</td>
<td>&#9679;</td>
</tr>
- <td align="center">Muxing B-Frames(bi-directional predicted frames)</td>
+ <td align="center">Muxing B-Frames (bi-directional predicted frames)</td>
+ <td>&#9675;</td>
<td>&#9675;</td>
<td>&#9675;</td>
<td>&#9675;</td>
<td>&#9675;</td>
<td>&#9675;</td>
<td>&#9675;</td>
- <td>&#8277;</td>
<td>&#8277;</td>
<td>&#8277;</td>
</tr>
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 4428adee818d..24e14e69637b 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -63,7 +63,7 @@ package android.nfc {
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isAutoChangeEnabled();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagPresent();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
- method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int);
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int, int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void pausePolling(int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void resumePolling();
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index 1eae3c6f30f1..8535e4a9cfd2 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -54,5 +54,5 @@ interface INfcCardEmulation
void setAutoChangeStatus(boolean state);
boolean isAutoChangeEnabled();
List<String> getRoutingStatus();
- void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech);
+ void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech, String sc);
}
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index fb63b5c03d00..bc410c7b8ba5 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -647,24 +647,29 @@ public final class NfcOemExtension {
* {@link ProtocolAndTechnologyRoute}
* @param emptyAid Zero-length AID route destination, where the possible inputs are defined in
* {@link ProtocolAndTechnologyRoute}
+ * @param systemCode System Code route destination, where the possible inputs are defined in
+ * {@link ProtocolAndTechnologyRoute}
*/
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public void overwriteRoutingTable(
@CardEmulation.ProtocolAndTechnologyRoute int protocol,
@CardEmulation.ProtocolAndTechnologyRoute int technology,
- @CardEmulation.ProtocolAndTechnologyRoute int emptyAid) {
+ @CardEmulation.ProtocolAndTechnologyRoute int emptyAid,
+ @CardEmulation.ProtocolAndTechnologyRoute int systemCode) {
String protocolRoute = routeIntToString(protocol);
String technologyRoute = routeIntToString(technology);
String emptyAidRoute = routeIntToString(emptyAid);
+ String systemCodeRoute = routeIntToString(systemCode);
NfcAdapter.callService(() ->
NfcAdapter.sCardEmulationService.overwriteRoutingTable(
mContext.getUser().getIdentifier(),
emptyAidRoute,
protocolRoute,
- technologyRoute
+ technologyRoute,
+ systemCodeRoute
));
}
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
index 39bbc25c9384..50419f7368be 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
@@ -263,8 +263,8 @@ public class CompanionAssociationActivity extends FragmentActivity implements
}
@Override
- protected void onStop() {
- super.onStop();
+ protected void onDestroy() {
+ super.onDestroy();
// TODO: handle config changes without cancelling.
if (!isDone()) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
index c48e7e40f77f..8df8a0761181 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt
@@ -33,7 +33,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.android.compose.rememberSystemUiController
-import com.android.compose.theme.LocalAndroidColorScheme
import androidx.compose.ui.unit.dp
import com.android.credentialmanager.common.material.ModalBottomSheetLayout
import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -57,7 +56,7 @@ fun ModalBottomSheet(
)
androidx.compose.material3.ModalBottomSheet(
onDismissRequest = onDismiss,
- containerColor = LocalAndroidColorScheme.current.surfaceBright,
+ containerColor = MaterialTheme.colorScheme.surfaceBright,
sheetState = state,
content = {
Box(
@@ -91,7 +90,7 @@ fun ModalBottomSheet(
setBottomSheetSystemBarsColor(sysUiController)
}
ModalBottomSheetLayout(
- sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright,
+ sheetBackgroundColor = MaterialTheme.colorScheme.surfaceBright,
modifier = Modifier.background(Color.Transparent),
sheetState = state,
sheetContent = { sheetContent() },
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
index 006a2d9858c4..426fec2c412e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt
@@ -29,12 +29,12 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.credentialmanager.ui.theme.Shapes
/**
@@ -54,7 +54,7 @@ fun SheetContainerCard(
modifier = modifier.fillMaxWidth().wrapContentHeight(),
border = null,
colors = CardDefaults.cardColors(
- containerColor = LocalAndroidColorScheme.current.surfaceBright,
+ containerColor = MaterialTheme.colorScheme.surfaceBright,
),
) {
if (topAppBar != null) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
index 2c3c63bea95f..84078c469892 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt
@@ -31,6 +31,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.SuggestionChipDefaults
import androidx.compose.runtime.Composable
@@ -52,7 +53,6 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.credentialmanager.ui.theme.EntryShape
import com.android.credentialmanager.ui.theme.Shapes
@@ -172,7 +172,7 @@ fun Entry(
// Decorative purpose only.
contentDescription = null,
modifier = Modifier.size(24.dp),
- tint = LocalAndroidColorScheme.current.onSurfaceVariant,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -186,7 +186,7 @@ fun Entry(
Icon(
modifier = iconSize,
bitmap = iconImageBitmap,
- tint = LocalAndroidColorScheme.current.onSurfaceVariant,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
@@ -210,7 +210,7 @@ fun Entry(
Icon(
modifier = iconSize,
imageVector = iconImageVector,
- tint = LocalAndroidColorScheme.current.onSurfaceVariant,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
@@ -222,7 +222,7 @@ fun Entry(
Icon(
modifier = iconSize,
painter = iconPainter,
- tint = LocalAndroidColorScheme.current.onSurfaceVariant,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
// Decorative purpose only.
contentDescription = null,
)
@@ -233,9 +233,9 @@ fun Entry(
},
border = null,
colors = SuggestionChipDefaults.suggestionChipColors(
- containerColor = LocalAndroidColorScheme.current.surfaceContainerHigh,
- labelColor = LocalAndroidColorScheme.current.onSurfaceVariant,
- iconContentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ labelColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ iconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
}
@@ -338,7 +338,7 @@ fun MoreOptionTopAppBar(
imageVector = navigationIcon,
contentDescription = navigationIconContentDescription,
modifier = Modifier.size(24.dp).autoMirrored(),
- tint = LocalAndroidColorScheme.current.onSurfaceVariant,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
index 342af3b134b0..37268ad42002 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt
@@ -21,23 +21,23 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
+import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
@Composable
fun CredentialListSectionHeader(text: String, isFirstSection: Boolean) {
InternalSectionHeader(
text = text,
- color = LocalAndroidColorScheme.current.onSurfaceVariant,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
applyTopPadding = !isFirstSection
)
}
@Composable
fun MoreAboutPasskeySectionHeader(text: String) {
- InternalSectionHeader(text, LocalAndroidColorScheme.current.onSurface)
+ InternalSectionHeader(text, MaterialTheme.colorScheme.onSurface)
}
@Composable
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
index b4075f1c4d80..d325ebb32579 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt
@@ -17,9 +17,9 @@
package com.android.credentialmanager.common.ui
import androidx.compose.runtime.Composable
+import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.graphics.Color
import com.android.compose.SystemUiController
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.credentialmanager.common.material.ModalBottomSheetDefaults
@Composable
@@ -34,7 +34,7 @@ fun setBottomSheetSystemBarsColor(sysUiController: SystemUiController) {
darkIcons = false
)
sysUiController.setNavigationBarColor(
- color = LocalAndroidColorScheme.current.surfaceBright,
+ color = MaterialTheme.colorScheme.surfaceBright,
darkIcons = false
)
} \ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
index 68c2244f7622..3e999cbde113 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt
@@ -26,7 +26,6 @@ import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.style.Hyphens
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import com.android.compose.theme.LocalAndroidColorScheme
/**
* The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X".
@@ -38,7 +37,7 @@ fun HeadlineText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentSize(),
text = text,
- color = LocalAndroidColorScheme.current.onSurface,
+ color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineSmall.copy(hyphens = Hyphens.Auto),
)
@@ -52,7 +51,7 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentSize(),
text = text,
- color = LocalAndroidColorScheme.current.onSurfaceVariant,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium.copy(hyphens = Hyphens.Auto),
)
}
@@ -70,7 +69,7 @@ fun BodySmallText(
Text(
modifier = modifier.wrapContentSize(),
text = text,
- color = LocalAndroidColorScheme.current.onSurfaceVariant,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall.copy(hyphens = Hyphens.Auto),
overflow = TextOverflow.Ellipsis,
maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
@@ -86,7 +85,7 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.wrapContentSize(),
text = text,
- color = LocalAndroidColorScheme.current.onSurface,
+ color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge.copy(hyphens = Hyphens.Auto),
)
}
@@ -104,7 +103,7 @@ fun SmallTitleText(
Text(
modifier = modifier.wrapContentSize(),
text = text,
- color = LocalAndroidColorScheme.current.onSurface,
+ color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleSmall.copy(hyphens = Hyphens.Auto),
overflow = TextOverflow.Ellipsis,
maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE,
@@ -160,7 +159,7 @@ fun LargeLabelTextOnSurfaceVariant(text: String, modifier: Modifier = Modifier)
modifier = modifier.wrapContentSize(),
text = text,
textAlign = TextAlign.Center,
- color = LocalAndroidColorScheme.current.onSurfaceVariant,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge.copy(hyphens = Hyphens.Auto),
)
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 4993a1fa0672..d78889151c78 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.NewReleases
import androidx.compose.material.icons.filled.Add
@@ -46,7 +47,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.toBitmap
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.credentialmanager.CredentialSelectorViewModel
import com.android.credentialmanager.R
import com.android.credentialmanager.common.BiometricError
@@ -448,7 +448,7 @@ fun CreationSelectionCard(
item {
Divider(
thickness = 1.dp,
- color = LocalAndroidColorScheme.current.outlineVariant,
+ color = MaterialTheme.colorScheme.outlineVariant,
modifier = Modifier.padding(vertical = 16.dp)
)
}
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
new file mode 100644
index 000000000000..952562e3d8ea
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:gravity="center_vertical"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:clipToPadding="false"
+ android:baselineAligned="false">
+
+ <include layout="@layout/settingslib_icon_frame"/>
+
+ <include layout="@layout/settingslib_preference_frame"/>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="end|center_vertical"
+ android:paddingLeft="16dp"
+ android:paddingStart="16dp"
+ android:paddingRight="0dp"
+ android:paddingEnd="0dp"
+ android:orientation="vertical"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 2a251a59e1d8..dfd296fe006f 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -39,6 +39,7 @@ import com.android.settingslib.spa.gallery.page.LoadingBarPageProvider
import com.android.settingslib.spa.gallery.page.ProgressBarPageProvider
import com.android.settingslib.spa.gallery.scaffold.NonScrollablePagerPageProvider
import com.android.settingslib.spa.gallery.page.SliderPageProvider
+import com.android.settingslib.spa.gallery.preference.CheckBoxPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.IntroPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.ListPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.MainSwitchPreferencePageProvider
@@ -46,6 +47,7 @@ import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
import com.android.settingslib.spa.gallery.preference.PreferencePageProvider
import com.android.settingslib.spa.gallery.preference.SwitchPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.TopIntroPreferencePageProvider
+import com.android.settingslib.spa.gallery.preference.TwoTargetButtonPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider
import com.android.settingslib.spa.gallery.preference.ZeroStatePreferencePageProvider
import com.android.settingslib.spa.gallery.scaffold.PagerMainPageProvider
@@ -105,6 +107,8 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) {
CopyablePageProvider,
IntroPreferencePageProvider,
TopIntroPreferencePageProvider,
+ CheckBoxPreferencePageProvider,
+ TwoTargetButtonPreferencePageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt
index c9c81aac01c3..cb055049e6c4 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/dialog/DialogMainPageProvider.kt
@@ -19,50 +19,93 @@ package com.android.settingslib.spa.gallery.dialog
import android.os.Bundle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import com.android.settingslib.spa.framework.common.SettingsEntry
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.widget.dialog.AlertDialogButton
+import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon
import com.android.settingslib.spa.widget.dialog.rememberAlertDialogPresenter
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
private const val TITLE = "Category: Dialog"
object DialogMainPageProvider : SettingsPageProvider {
override val name = "DialogMain"
- private val owner = createSettingsPage()
- override fun buildEntry(arguments: Bundle?): List<SettingsEntry> = listOf(
- SettingsEntryBuilder.create("AlertDialog", owner).setUiLayoutFn {
- val alertDialogPresenter = rememberAlertDialogPresenter(
- confirmButton = AlertDialogButton("Ok"),
- dismissButton = AlertDialogButton("Cancel"),
- title = "Title",
- text = { Text("Text") },
- )
- Preference(object : PreferenceModel {
- override val title = "Show AlertDialog"
- override val onClick = alertDialogPresenter::open
- })
- }.build(),
- SettingsEntryBuilder.create("NavDialog", owner).setUiLayoutFn {
- Preference(object : PreferenceModel {
- override val title = "Navigate to Dialog"
- override val onClick = navigator(route = NavDialogProvider.name)
- })
- }.build(),
- )
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(TITLE) {
+ Category {
+ AlertDialog()
+ AlertDialogWithIcon()
+ NavDialog()
+ }
+ }
+ }
@Composable
fun Entry() {
- Preference(object : PreferenceModel {
- override val title = TITLE
- override val onClick = navigator(name)
- })
+ Preference(
+ object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ }
+ )
}
override fun getTitle(arguments: Bundle?) = TITLE
}
+
+@Composable
+private fun AlertDialog() {
+ val alertDialogPresenter =
+ rememberAlertDialogPresenter(
+ confirmButton = AlertDialogButton("Ok"),
+ dismissButton = AlertDialogButton("Cancel"),
+ title = "Title",
+ text = { Text("Text") },
+ )
+ Preference(
+ object : PreferenceModel {
+ override val title = "Show AlertDialog"
+ override val onClick = alertDialogPresenter::open
+ }
+ )
+}
+
+@Composable
+private fun AlertDialogWithIcon() {
+ var openDialog by rememberSaveable { mutableStateOf(false) }
+ val close = { openDialog = false }
+ val open = { openDialog = true }
+ if (openDialog) {
+ SettingsAlertDialogWithIcon(
+ title = "Title",
+ onDismissRequest = close,
+ confirmButton = AlertDialogButton("OK", onClick = close),
+ dismissButton = AlertDialogButton("Dismiss", onClick = close),
+ ) {}
+ }
+ Preference(
+ object : PreferenceModel {
+ override val title = "Show AlertDialogWithIcon"
+ override val onClick = open
+ }
+ )
+}
+
+@Composable
+private fun NavDialog() {
+ Preference(
+ object : PreferenceModel {
+ override val title = "Navigate to Dialog"
+ override val onClick = navigator(route = NavDialogProvider.name)
+ }
+ )
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt
new file mode 100644
index 000000000000..c2b67cbfe56f
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/CheckBoxPreferencePageProvider.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.settingslib.spa.gallery.preference
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AirplanemodeActive
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.widget.preference.CheckboxPreference
+import com.android.settingslib.spa.widget.preference.CheckboxPreferenceModel
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
+import com.android.settingslib.spa.widget.ui.SettingsIcon
+
+private const val TITLE = "Sample CheckBoxPreference"
+
+object CheckBoxPreferencePageProvider : SettingsPageProvider {
+ override val name = "CheckBoxPreference"
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(TITLE) {
+ Category {
+ var checked1 by rememberSaveable { mutableStateOf(true) }
+ CheckboxPreference(
+ object : CheckboxPreferenceModel {
+ override val title = "Use Dark theme"
+ override val checked = { checked1 }
+ override val onCheckedChange = { newChecked: Boolean ->
+ checked1 = newChecked
+ }
+ }
+ )
+ var checked2 by rememberSaveable { mutableStateOf(false) }
+ CheckboxPreference(
+ object : CheckboxPreferenceModel {
+ override val title = "Use Dark theme"
+ override val summary = { "Summary" }
+ override val checked = { checked2 }
+ override val onCheckedChange = { newChecked: Boolean ->
+ checked2 = newChecked
+ }
+ }
+ )
+ var checked3 by rememberSaveable { mutableStateOf(true) }
+ CheckboxPreference(
+ object : CheckboxPreferenceModel {
+ override val title = "Use Dark theme"
+ override val summary = { "Summary" }
+ override val checked = { checked3 }
+ override val onCheckedChange = { newChecked: Boolean ->
+ checked3 = newChecked
+ }
+ override val icon =
+ @Composable {
+ SettingsIcon(imageVector = Icons.Outlined.AirplanemodeActive)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun Entry() {
+ Preference(
+ object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ }
+ )
+ }
+
+ override fun getTitle(arguments: Bundle?): String {
+ return TITLE
+ }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt
index 831b43942e98..3cfb5368e24a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMainPageProvider.kt
@@ -36,11 +36,13 @@ object PreferenceMainPageProvider : SettingsPageProvider {
Category {
PreferencePageProvider.Entry()
ListPreferencePageProvider.Entry()
+ CheckBoxPreferencePageProvider.Entry()
}
Category {
SwitchPreferencePageProvider.Entry()
MainSwitchPreferencePageProvider.Entry()
TwoTargetSwitchPreferencePageProvider.Entry()
+ TwoTargetButtonPreferencePageProvider.Entry()
}
Category {
ZeroStatePreferencePageProvider.Entry()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt
new file mode 100644
index 000000000000..c6e834a3fa7b
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetButtonPreferencePageProvider.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.preference
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Add
+import androidx.compose.material.icons.outlined.Info
+import androidx.compose.runtime.Composable
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.preference.TwoTargetButtonPreference
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.Category
+
+private const val TITLE = "Sample TwoTargetButtonPreference"
+
+object TwoTargetButtonPreferencePageProvider : SettingsPageProvider {
+ override val name = "TwoTargetButtonPreference"
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(TITLE) {
+ Category {
+ SampleTwoTargetButtonPreference()
+ SampleTwoTargetButtonPreferenceWithSummary()
+ }
+ }
+ }
+
+ @Composable
+ fun Entry() {
+ Preference(
+ object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ }
+ )
+ }
+}
+
+@Composable
+private fun SampleTwoTargetButtonPreference() {
+ TwoTargetButtonPreference(
+ title = "TwoTargetButton",
+ summary = { "" },
+ buttonIcon = Icons.Outlined.Info,
+ buttonIconDescription = "info",
+ onClick = {},
+ onButtonClick = {},
+ )
+}
+
+@Composable
+private fun SampleTwoTargetButtonPreferenceWithSummary() {
+ TwoTargetButtonPreference(
+ title = "TwoTargetButton",
+ summary = { "summary" },
+ buttonIcon = Icons.Outlined.Add,
+ buttonIconDescription = "info",
+ onClick = {},
+ onButtonClick = {},
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index ab95162fb142..5dca63724faf 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -85,4 +85,6 @@ object SettingsDimension {
val illustrationMaxHeight = 300.dp
val illustrationPadding = paddingLarge
val illustrationCornerRadius = 28.dp
+
+ val preferenceMinHeight = 72.dp
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
index 58a83fa72532..4cf270dca2bb 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt
@@ -17,7 +17,6 @@
package com.android.settingslib.spa.widget.dialog
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -26,12 +25,13 @@ import androidx.compose.material.icons.filled.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.window.DialogProperties
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
@Composable
fun SettingsAlertDialogWithIcon(
@@ -57,7 +57,9 @@ fun SettingsAlertDialogWithIcon(
title?.let {
{
CenterRow {
- Text(it, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
+ if (isSpaExpressiveEnabled)
+ Text(it, style = MaterialTheme.typography.bodyLarge)
+ else Text(it)
}
}
},
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
index c68ec78b1ba6..acb96be64a34 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -35,6 +36,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled
import com.android.settingslib.spa.framework.theme.SettingsShape
@@ -62,7 +64,8 @@ internal fun BaseLayout(
.semantics(mergeDescendants = true) {}
.then(
if (isSpaExpressiveEnabled)
- Modifier.clip(SettingsShape.CornerExtraSmall)
+ Modifier.heightIn(min = SettingsDimension.preferenceMinHeight)
+ .clip(SettingsShape.CornerExtraSmall)
.background(MaterialTheme.colorScheme.surfaceBright)
else Modifier
)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
index b28e88eb8af8..e6a23662e9f0 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/MainSwitchPreference.kt
@@ -17,6 +17,7 @@
package com.android.settingslib.spa.widget.preference
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
@@ -25,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsShape
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -35,13 +37,19 @@ import com.android.settingslib.spa.framework.util.EntryHighlight
fun MainSwitchPreference(model: SwitchPreferenceModel) {
EntryHighlight {
Surface(
- modifier = Modifier.padding(SettingsDimension.itemPaddingEnd),
- color = when (model.checked()) {
- true -> MaterialTheme.colorScheme.primaryContainer
- else -> MaterialTheme.colorScheme.secondaryContainer
- },
- shape = if (isSpaExpressiveEnabled) CircleShape
- else SettingsShape.CornerExtraLarge,
+ modifier =
+ Modifier.padding(SettingsDimension.itemPaddingEnd)
+ .then(
+ if (isSpaExpressiveEnabled)
+ Modifier.heightIn(min = SettingsDimension.preferenceMinHeight)
+ else Modifier
+ ),
+ color =
+ when (model.checked()) {
+ true -> MaterialTheme.colorScheme.primaryContainer
+ else -> MaterialTheme.colorScheme.secondaryContainer
+ },
+ shape = if (isSpaExpressiveEnabled) CircleShape else SettingsShape.CornerExtraLarge,
) {
InternalSwitchPreference(
title = model.title,
@@ -61,16 +69,20 @@ fun MainSwitchPreference(model: SwitchPreferenceModel) {
private fun MainSwitchPreferencePreview() {
SettingsTheme {
Column {
- MainSwitchPreference(object : SwitchPreferenceModel {
- override val title = "Use Dark theme"
- override val checked = { true }
- override val onCheckedChange: (Boolean) -> Unit = {}
- })
- MainSwitchPreference(object : SwitchPreferenceModel {
- override val title = "Use Dark theme"
- override val checked = { false }
- override val onCheckedChange: (Boolean) -> Unit = {}
- })
+ MainSwitchPreference(
+ object : SwitchPreferenceModel {
+ override val title = "Use Dark theme"
+ override val checked = { true }
+ override val onCheckedChange: (Boolean) -> Unit = {}
+ }
+ )
+ MainSwitchPreference(
+ object : SwitchPreferenceModel {
+ override val title = "Use Dark theme"
+ override val checked = { false }
+ override val onCheckedChange: (Boolean) -> Unit = {}
+ }
+ )
}
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt
index b771f367e697..541922335387 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/ZeroStatePreference.kt
@@ -54,7 +54,7 @@ fun ZeroStatePreference(icon: ImageVector, text: String? = null, description: St
val zeroStateShape = remember {
RoundedPolygon.star(
numVerticesPerRadius = 6,
- innerRadius = 0.75f,
+ innerRadius = 0.8f,
rounding = CornerRounding(0.3f)
)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 8b6351e3c289..7fdb32cb63e9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -60,7 +60,7 @@ public class CachedBluetoothDeviceManager {
mBtManager = localBtManager;
mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager,
mCachedDevices);
- mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices);
+ mCsipDeviceManager = new CsipDeviceManager(context, localBtManager, mCachedDevices);
}
public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index 6dab22454baf..b9f16edf6e77 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -21,8 +21,10 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
+import android.content.Context;
import android.os.Build;
import android.os.ParcelUuid;
+import android.os.UserManager;
import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
@@ -45,9 +47,11 @@ public class CsipDeviceManager {
private final LocalBluetoothManager mBtManager;
private final List<CachedBluetoothDevice> mCachedDevices;
+ private final Context mContext;
- CsipDeviceManager(LocalBluetoothManager localBtManager,
+ CsipDeviceManager(Context context, LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> cachedDevices) {
+ mContext = context;
mBtManager = localBtManager;
mCachedDevices = cachedDevices;
}
@@ -379,7 +383,11 @@ public class CsipDeviceManager {
preferredMainDevice.refresh();
hasChanged = true;
}
- syncAudioSharingSourceIfNeeded(preferredMainDevice);
+ if (isWorkProfile()) {
+ log("addMemberDevicesIntoMainDevice: skip sync source for work profile");
+ } else {
+ syncAudioSharingSourceIfNeeded(preferredMainDevice);
+ }
}
if (hasChanged) {
log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
@@ -388,6 +396,11 @@ public class CsipDeviceManager {
return hasChanged;
}
+ private boolean isWorkProfile() {
+ UserManager userManager = mContext.getSystemService(UserManager.class);
+ return userManager != null && userManager.isManagedProfile();
+ }
+
private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) {
boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingEnabled();
if (isAudioSharingEnabled) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 364e95c61ca8..6a9d5687370e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -18,6 +18,8 @@ package com.android.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+import static java.util.stream.Collectors.toList;
+
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.bluetooth.BluetoothAdapter;
@@ -64,6 +66,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
@@ -84,6 +87,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
public static final String EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE = "BT_DEVICE_TO_AUTO_ADD_SOURCE";
public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING";
public static final String EXTRA_PAIR_AND_JOIN_SHARING = "PAIR_AND_JOIN_SHARING";
+ public static final String BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID =
+ "bluetooth_le_broadcast_primary_device_group_id";
public static final int BROADCAST_STATE_UNKNOWN = 0;
public static final int BROADCAST_STATE_ON = 1;
public static final int BROADCAST_STATE_OFF = 2;
@@ -1121,6 +1126,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
/** Update fallback active device if needed. */
public void updateFallbackActiveDeviceIfNeeded() {
+ if (isWorkProfile(mContext)) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded for work profile.");
+ return;
+ }
if (mServiceBroadcast == null) {
Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to broadcast profile is null");
return;
@@ -1135,71 +1144,114 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null");
return;
}
- List<BluetoothDevice> devicesInBroadcast = getDevicesInBroadcast();
- if (devicesInBroadcast.isEmpty()) {
+ Map<Integer, List<BluetoothDevice>> deviceGroupsInBroadcast = getDeviceGroupsInBroadcast();
+ if (deviceGroupsInBroadcast.isEmpty()) {
Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast");
return;
}
- List<BluetoothDevice> devices =
- BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
- BluetoothDevice targetDevice = null;
- // Find the earliest connected device in sharing session.
- int targetDeviceIdx = -1;
- for (BluetoothDevice device : devicesInBroadcast) {
- if (devices.contains(device)) {
- int idx = devices.indexOf(device);
- if (idx > targetDeviceIdx) {
- targetDeviceIdx = idx;
- targetDevice = device;
+ int targetGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ int fallbackActiveGroupId = BluetoothUtils.getPrimaryGroupIdForBroadcast(
+ mContext.getContentResolver());
+ if (Flags.audioSharingHysteresisModeFix()) {
+ int userPreferredPrimaryGroupId = getUserPreferredPrimaryGroupId();
+ if (userPreferredPrimaryGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+ && deviceGroupsInBroadcast.containsKey(userPreferredPrimaryGroupId)) {
+ if (userPreferredPrimaryGroupId == fallbackActiveGroupId) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, already user preferred");
+ return;
+ } else {
+ targetGroupId = userPreferredPrimaryGroupId;
}
}
- }
- if (targetDevice == null) {
- Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null");
+ if (targetGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ // If there is no user preferred primary device, set the earliest connected
+ // device in sharing session as the fallback.
+ targetGroupId = getEarliestConnectedDeviceGroup(deviceGroupsInBroadcast);
+ }
+ } else {
+ // Set the earliest connected device in sharing session as the fallback.
+ targetGroupId = getEarliestConnectedDeviceGroup(deviceGroupsInBroadcast);
+ }
+ Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, target group id = " + targetGroupId);
+ if (targetGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return;
+ if (targetGroupId == fallbackActiveGroupId) {
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, already is fallback");
return;
}
- CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice);
+ CachedBluetoothDevice targetCachedDevice = getMainDevice(
+ deviceGroupsInBroadcast.get(targetGroupId));
if (targetCachedDevice == null) {
- Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device");
- return;
- }
- int fallbackActiveGroupId = getFallbackActiveGroupId();
- if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
- && BluetoothUtils.getGroupId(targetCachedDevice) == fallbackActiveGroupId) {
- Log.d(
- TAG,
- "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: "
- + fallbackActiveGroupId);
+ Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find main device");
return;
}
Log.d(
TAG,
"updateFallbackActiveDeviceIfNeeded, set active device: "
- + targetDevice.getAnonymizedAddress());
+ + targetCachedDevice.getDevice());
targetCachedDevice.setActive();
}
- private List<BluetoothDevice> getDevicesInBroadcast() {
+ @NonNull
+ private Map<Integer, List<BluetoothDevice>> getDeviceGroupsInBroadcast() {
boolean hysteresisModeFixEnabled = Flags.audioSharingHysteresisModeFix();
List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices();
return connectedDevices.stream()
.filter(
- bluetoothDevice -> {
+ device -> {
List<BluetoothLeBroadcastReceiveState> sourceList =
- mServiceBroadcastAssistant.getAllSources(
- bluetoothDevice);
+ mServiceBroadcastAssistant.getAllSources(device);
return !sourceList.isEmpty() && sourceList.stream().anyMatch(
source -> hysteresisModeFixEnabled
? BluetoothUtils.isSourceMatched(source, mBroadcastId)
: BluetoothUtils.isConnected(source));
})
- .collect(Collectors.toList());
+ .collect(Collectors.groupingBy(
+ device -> BluetoothUtils.getGroupId(mDeviceManager.findDevice(device))));
+ }
+
+ private int getEarliestConnectedDeviceGroup(
+ @NonNull Map<Integer, List<BluetoothDevice>> deviceGroups) {
+ List<BluetoothDevice> devices =
+ BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
+ // Find the earliest connected device in sharing session.
+ int targetDeviceIdx = -1;
+ int targetGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ for (Map.Entry<Integer, List<BluetoothDevice>> entry : deviceGroups.entrySet()) {
+ for (BluetoothDevice device : entry.getValue()) {
+ if (devices.contains(device)) {
+ int idx = devices.indexOf(device);
+ if (idx > targetDeviceIdx) {
+ targetDeviceIdx = idx;
+ targetGroupId = entry.getKey();
+ }
+ }
+ }
+ }
+ return targetGroupId;
+ }
+
+ @Nullable
+ private CachedBluetoothDevice getMainDevice(@Nullable List<BluetoothDevice> devices) {
+ if (devices == null || devices.size() == 1) return null;
+ List<CachedBluetoothDevice> cachedDevices =
+ devices.stream()
+ .map(device -> mDeviceManager.findDevice(device))
+ .filter(Objects::nonNull)
+ .collect(toList());
+ for (CachedBluetoothDevice cachedDevice : cachedDevices) {
+ if (!cachedDevice.getMemberDevice().isEmpty()) {
+ return cachedDevice;
+ }
+ }
+ CachedBluetoothDevice mainDevice = cachedDevices.isEmpty() ? null : cachedDevices.get(0);
+ return mainDevice;
}
- private int getFallbackActiveGroupId() {
+ private int getUserPreferredPrimaryGroupId() {
+ // TODO: use real key name in SettingsProvider
return Settings.Secure.getInt(
- mContext.getContentResolver(),
- "bluetooth_le_broadcast_fallback_active_group_id",
+ mContentResolver,
+ BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID,
BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
index 24815fabfdf2..91a99aed6db5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistantCallbackExt.kt
@@ -16,7 +16,6 @@
package com.android.settingslib.bluetooth
-import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothLeBroadcastAssistant
import android.bluetooth.BluetoothLeBroadcastMetadata
@@ -82,9 +81,5 @@ val LocalBluetoothLeBroadcastAssistant.onSourceConnectedOrRemoved: Flow<Unit>
ConcurrentUtils.DIRECT_EXECUTOR,
callback,
)
- awaitClose {
- if (BluetoothAdapter.getDefaultAdapter()?.isEnabled == true) {
- unregisterServiceCallBack(callback)
- }
- }
+ awaitClose { unregisterServiceCallBack(callback) }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
index 0bcf7fed5c80..07abb6b912b6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
@@ -68,3 +68,44 @@ val LocalBluetoothLeBroadcast.onBroadcastStartedOrStopped: Flow<Unit>
awaitClose { unregisterServiceCallBack(listener) }
}
.buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [BluetoothLeBroadcast.Callback] onPlaybackStarted event */
+val LocalBluetoothLeBroadcast.onPlaybackStarted: Flow<Unit>
+ get() =
+ callbackFlow {
+ val listener =
+ object : BluetoothLeBroadcast.Callback {
+ override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
+
+ override fun onBroadcastStartFailed(reason: Int) {
+ }
+
+ override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
+ }
+
+ override fun onBroadcastStopFailed(reason: Int) {
+ }
+
+ override fun onPlaybackStarted(reason: Int, broadcastId: Int) {
+ launch { trySend(Unit) }
+ }
+
+ override fun onPlaybackStopped(reason: Int, broadcastId: Int) {
+ }
+
+ override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
+
+ override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
+
+ override fun onBroadcastMetadataChanged(
+ broadcastId: Int,
+ metadata: BluetoothLeBroadcastMetadata
+ ) {}
+ }
+ registerServiceCallBack(
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ listener,
+ )
+ awaitClose { unregisterServiceCallBack(listener) }
+ }
+ .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
index 9cf49070a62c..c85756ed067a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsConfigProviderService.aidl
@@ -20,5 +20,5 @@ import com.android.settingslib.bluetooth.devicesettings.DeviceInfo;
import com.android.settingslib.bluetooth.devicesettings.IGetDeviceSettingsConfigCallback;
interface IDeviceSettingsConfigProviderService {
- oneway void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback);
+ void getDeviceSettingsConfig(in DeviceInfo device, in IGetDeviceSettingsConfigCallback callback);
} \ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 4af0504bd73a..a33fcc6747b4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -23,6 +23,7 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
+import android.os.RemoteException
import android.text.TextUtils
import android.util.Log
import com.android.settingslib.bluetooth.BluetoothUtils
@@ -55,6 +56,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
@@ -100,6 +102,9 @@ class DeviceSettingServiceConnection(
private var isServiceEnabled =
coroutineScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
val states = getSettingsProviderServices()?.values ?: return@async false
+ if (states.isEmpty()) {
+ return@async true
+ }
combine(states) { it.toList() }
.mapNotNull { allStatus ->
if (allStatus.any { it is ServiceConnectionStatus.Failed }) {
@@ -114,7 +119,7 @@ class DeviceSettingServiceConnection(
null
}
}
- .first()
+ .firstOrNull() ?: false
}
private var config =
@@ -131,9 +136,15 @@ class DeviceSettingServiceConnection(
is ServiceConnectionStatus.Connected ->
flowOf(
getDeviceSettingsConfigFromService(
- deviceInfo { setBluetoothAddress(cachedDevice.address) },
- it.service,
- )
+ deviceInfo { setBluetoothAddress(cachedDevice.address) },
+ it.service,
+ )
+ .also { config ->
+ Log.i(
+ TAG,
+ "device setting config for $cachedDevice is $config",
+ )
+ }
)
ServiceConnectionStatus.Connecting -> flowOf()
ServiceConnectionStatus.Failed -> flowOf(null)
@@ -146,21 +157,26 @@ class DeviceSettingServiceConnection(
deviceInfo: DeviceInfo,
service: IDeviceSettingsConfigProviderService,
): DeviceSettingsConfig? = suspendCancellableCoroutine { continuation ->
- service.getDeviceSettingsConfig(
- deviceInfo,
- object : IGetDeviceSettingsConfigCallback.Stub() {
- override fun onResult(
- status: DeviceSettingsConfigServiceStatus,
- config: DeviceSettingsConfig?,
- ) {
- if (!status.success) {
- continuation.resume(null)
- } else {
- continuation.resume(config)
+ try {
+ service.getDeviceSettingsConfig(
+ deviceInfo,
+ object : IGetDeviceSettingsConfigCallback.Stub() {
+ override fun onResult(
+ status: DeviceSettingsConfigServiceStatus,
+ config: DeviceSettingsConfig?,
+ ) {
+ if (!status.success) {
+ continuation.resume(null)
+ } else {
+ continuation.resume(config)
+ }
}
- }
- },
- )
+ },
+ )
+ } catch (e: RemoteException) {
+ Log.i(TAG, "Fail to get config")
+ continuation.resume(null)
+ }
}
private val settingIdToItemMapping =
@@ -298,13 +314,16 @@ class DeviceSettingServiceConnection(
val serviceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
+ Log.i(TAG, "Service connected for $intent")
launch { send(ServiceConnectionStatus.Connected(transform(service))) }
}
override fun onServiceDisconnected(name: ComponentName?) {
+ Log.i(TAG, "Service disconnected for $intent")
launch { send(ServiceConnectionStatus.Connecting) }
}
}
+ Log.i(TAG, "Try to bind service for $intent")
if (
!context.bindService(
intent,
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
index 1d17b004fb6a..6335e712f904 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -49,19 +49,23 @@ public class InputMediaDevice extends MediaDevice {
private final boolean mIsVolumeFixed;
+ private final String mProductName;
+
private InputMediaDevice(
@NonNull Context context,
@NonNull String id,
@AudioDeviceType int audioDeviceInfoType,
int maxVolume,
int currentVolume,
- boolean isVolumeFixed) {
+ boolean isVolumeFixed,
+ @Nullable String productName) {
super(context, /* info= */ null, /* item= */ null);
mId = id;
mAudioDeviceInfoType = audioDeviceInfoType;
mMaxVolume = maxVolume;
mCurrentVolume = currentVolume;
mIsVolumeFixed = isVolumeFixed;
+ mProductName = productName;
initDeviceRecord();
}
@@ -72,13 +76,20 @@ public class InputMediaDevice extends MediaDevice {
@AudioDeviceType int audioDeviceInfoType,
int maxVolume,
int currentVolume,
- boolean isVolumeFixed) {
+ boolean isVolumeFixed,
+ @Nullable String productName) {
if (!isSupportedInputDevice(audioDeviceInfoType)) {
return null;
}
return new InputMediaDevice(
- context, id, audioDeviceInfoType, maxVolume, currentVolume, isVolumeFixed);
+ context,
+ id,
+ audioDeviceInfoType,
+ maxVolume,
+ currentVolume,
+ isVolumeFixed,
+ productName);
}
public @AudioDeviceType int getAudioDeviceInfoType() {
@@ -98,18 +109,25 @@ public class InputMediaDevice extends MediaDevice {
};
}
+ @Nullable
+ public String getProductName() {
+ return mProductName;
+ }
+
@Override
public @NonNull String getName() {
- CharSequence name = switch (mAudioDeviceInfoType) {
- case TYPE_WIRED_HEADSET -> mContext.getString(
- R.string.media_transfer_wired_device_mic_name);
- case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY -> mContext.getString(
- R.string.media_transfer_usb_device_mic_name);
- case TYPE_BLUETOOTH_SCO -> mContext.getString(
- R.string.media_transfer_bt_device_mic_name);
+ return switch (mAudioDeviceInfoType) {
+ case TYPE_WIRED_HEADSET ->
+ mContext.getString(R.string.media_transfer_wired_device_mic_name);
+ case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY ->
+ // The product name is assumed to be a well-formed string if it's not null.
+ mProductName != null
+ ? mProductName
+ : mContext.getString(R.string.media_transfer_usb_device_mic_name);
+ case TYPE_BLUETOOTH_SCO ->
+ mContext.getString(R.string.media_transfer_bt_device_mic_name);
default -> mContext.getString(R.string.media_transfer_this_device_name_desktop);
};
- return name.toString();
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index a72ba8db848d..4f315a2a2486 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -22,6 +22,7 @@ import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
+import android.media.AudioDeviceInfo.AudioDeviceType;
import android.media.AudioManager;
import android.media.MediaRecorder;
import android.os.Handler;
@@ -63,7 +64,7 @@ public final class InputRouteManager {
@VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
- private MediaDevice mSelectedInputDevice;
+ private @AudioDeviceType int mSelectedInputDeviceType;
private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
private final Object mCallbackLock = new Object();
@@ -73,12 +74,12 @@ public final class InputRouteManager {
new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
- dispatchInputDeviceListUpdate();
+ applyDefaultSelectedTypeToAllPresets();
}
@Override
public void onAudioDevicesRemoved(@NonNull AudioDeviceInfo[] removedDevices) {
- dispatchInputDeviceListUpdate();
+ applyDefaultSelectedTypeToAllPresets();
}
};
@@ -92,9 +93,12 @@ public final class InputRouteManager {
mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener(
new HandlerExecutor(handler),
this::onPreferredDevicesForCapturePresetChangedListener);
+
+ applyDefaultSelectedTypeToAllPresets();
}
- private void onPreferredDevicesForCapturePresetChangedListener(
+ @VisibleForTesting
+ void onPreferredDevicesForCapturePresetChangedListener(
@MediaRecorder.SystemSource int capturePreset,
@NonNull List<AudioDeviceAttributes> devices) {
if (capturePreset == MediaRecorder.AudioSource.MIC) {
@@ -117,12 +121,30 @@ public final class InputRouteManager {
}
}
+ // TODO(b/355684672): handle edge case where there are two devices with the same type. Only
+ // using a single mSelectedInputDeviceType might not be enough to recognize the correct device.
public @Nullable MediaDevice getSelectedInputDevice() {
- return mSelectedInputDevice;
+ for (MediaDevice device : mInputMediaDevices) {
+ if (((InputMediaDevice) device).getAudioDeviceInfoType() == mSelectedInputDeviceType) {
+ return device;
+ }
+ }
+ return null;
}
- private void dispatchInputDeviceListUpdate() {
- // Get selected input device.
+ private void applyDefaultSelectedTypeToAllPresets() {
+ mSelectedInputDeviceType = retrieveDefaultSelectedDeviceType();
+ AudioDeviceAttributes deviceAttributes =
+ createInputDeviceAttributes(mSelectedInputDeviceType);
+ setPreferredDeviceForAllPresets(deviceAttributes);
+ }
+
+ private AudioDeviceAttributes createInputDeviceAttributes(@AudioDeviceType int type) {
+ // Address is not used.
+ return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_INPUT, type, /* address= */ "");
+ }
+
+ private @AudioDeviceType int retrieveDefaultSelectedDeviceType() {
List<AudioDeviceAttributes> attributesOfSelectedInputDevices =
mAudioManager.getDevicesForAttributes(INPUT_ATTRIBUTES);
int selectedInputDeviceAttributesType;
@@ -138,7 +160,10 @@ public final class InputRouteManager {
}
selectedInputDeviceAttributesType = attributesOfSelectedInputDevices.get(0).getType();
}
+ return selectedInputDeviceAttributesType;
+ }
+ private void dispatchInputDeviceListUpdate() {
// Get all input devices.
AudioDeviceInfo[] audioDeviceInfos =
mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
@@ -151,11 +176,11 @@ public final class InputRouteManager {
info.getType(),
getMaxInputGain(),
getCurrentInputGain(),
- isInputGainFixed());
+ isInputGainFixed(),
+ getProductNameFromAudioDeviceInfo(info));
if (mediaDevice != null) {
- if (info.getType() == selectedInputDeviceAttributesType) {
+ if (info.getType() == mSelectedInputDeviceType) {
mediaDevice.setState(STATE_SELECTED);
- mSelectedInputDevice = mediaDevice;
}
mInputMediaDevices.add(mediaDevice);
}
@@ -169,13 +194,32 @@ public final class InputRouteManager {
}
}
+ /**
+ * Gets the product name for the given {@link AudioDeviceInfo}.
+ *
+ * @return The product name for the given {@link AudioDeviceInfo}, or null if a suitable name
+ * cannot be found.
+ */
+ @Nullable
+ private String getProductNameFromAudioDeviceInfo(AudioDeviceInfo deviceInfo) {
+ CharSequence productName = deviceInfo.getProductName();
+ if (productName == null) {
+ return null;
+ }
+ String productNameString = productName.toString();
+ if (productNameString.isBlank()) {
+ return null;
+ }
+ return productNameString;
+ }
+
public void selectDevice(@NonNull MediaDevice device) {
- if (!(device instanceof InputMediaDevice)) {
+ if (!(device instanceof InputMediaDevice inputMediaDevice)) {
Slog.w(TAG, "This device is not an InputMediaDevice: " + device.getName());
return;
}
- if (device.equals(mSelectedInputDevice)) {
+ if (inputMediaDevice.getAudioDeviceInfoType() == mSelectedInputDeviceType) {
Slog.w(TAG, "This device is already selected: " + device.getName());
return;
}
@@ -186,12 +230,11 @@ public final class InputRouteManager {
return;
}
- // TODO(b/355684672): apply address for BT devices.
+ // Update mSelectedInputDeviceType directly based on user action.
+ mSelectedInputDeviceType = inputMediaDevice.getAudioDeviceInfoType();
+
AudioDeviceAttributes deviceAttributes =
- new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_INPUT,
- ((InputMediaDevice) device).getAudioDeviceInfoType(),
- /* address= */ "");
+ createInputDeviceAttributes(inputMediaDevice.getAudioDeviceInfoType());
try {
setPreferredDeviceForAllPresets(deviceAttributes);
} catch (IllegalArgumentException e) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index feaf7fbc4b64..b94e9069da6b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -15,6 +15,7 @@
*/
package com.android.settingslib.media;
+import static android.content.pm.PackageManager.FEATURE_PC;
import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
import static android.media.MediaRoute2Info.TYPE_DOCK;
import static android.media.MediaRoute2Info.TYPE_HDMI;
@@ -29,8 +30,6 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
import android.Manifest;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
@@ -43,6 +42,8 @@ import android.os.SystemProperties;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.settingslib.R;
import com.android.settingslib.media.flags.Flags;
@@ -72,7 +73,7 @@ public class PhoneMediaDevice extends MediaDevice {
return context.getString(R.string.media_transfer_this_device_name_tv);
} else if (isTablet()) {
return context.getString(R.string.media_transfer_this_device_name_tablet);
- } else if (inputRoutingEnabledAndIsDesktop()) {
+ } else if (inputRoutingEnabledAndIsDesktop(context)) {
return context.getString(R.string.media_transfer_this_device_name_desktop);
} else {
return context.getString(R.string.media_transfer_this_device_name);
@@ -88,7 +89,7 @@ public class PhoneMediaDevice extends MediaDevice {
case TYPE_WIRED_HEADSET:
case TYPE_WIRED_HEADPHONES:
name =
- inputRoutingEnabledAndIsDesktop()
+ inputRoutingEnabledAndIsDesktop(context)
? context.getString(R.string.media_transfer_headphone_name)
: context.getString(R.string.media_transfer_wired_headphone_name);
break;
@@ -96,7 +97,7 @@ public class PhoneMediaDevice extends MediaDevice {
case TYPE_USB_HEADSET:
case TYPE_USB_ACCESSORY:
name =
- inputRoutingEnabledAndIsDesktop()
+ inputRoutingEnabledAndIsDesktop(context)
? context.getString(R.string.media_transfer_usb_audio_name)
: context.getString(R.string.media_transfer_wired_headphone_name);
break;
@@ -149,14 +150,13 @@ public class PhoneMediaDevice extends MediaDevice {
.contains("tablet");
}
- static boolean isDesktop() {
- return Arrays.asList(SystemProperties.get("ro.build.characteristics").split(","))
- .contains("desktop");
+ public static boolean isDesktop(@NonNull Context context) {
+ return context.getPackageManager().hasSystemFeature(FEATURE_PC);
}
- static boolean inputRoutingEnabledAndIsDesktop() {
+ public static boolean inputRoutingEnabledAndIsDesktop(@NonNull Context context) {
return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl()
- && isDesktop();
+ && isDesktop(context);
}
// MediaRoute2Info.getType was made public on API 34, but exists since API 30.
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 43d79466d6ca..23be7baa6496 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -78,6 +78,10 @@ class FakeZenModeRepository : ZenModeRepository {
mutableModesFlow.value = (mutableModesFlow.value.filter { it.id != modeId }) + mode
}
+ fun clearModes() {
+ mutableModesFlow.value = listOf()
+ }
+
fun getMode(id: String): ZenMode? {
return mutableModesFlow.value.find { it.id == id }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
index 2f8105ae461d..b41e9703d427 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
@@ -74,6 +74,8 @@ interface AudioSharingRepository {
/** The headset groupId to volume map during audio sharing. */
val volumeMap: StateFlow<GroupIdToVolumes>
+ suspend fun audioSharingAvailable(): Boolean
+
/** Set the volume of secondary headset during audio sharing. */
suspend fun setSecondaryVolume(
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
@@ -216,6 +218,12 @@ class AudioSharingRepositoryImpl(
}
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap())
+ override suspend fun audioSharingAvailable(): Boolean {
+ return withContext(backgroundCoroutineContext) {
+ BluetoothUtils.isAudioSharingEnabled()
+ }
+ }
+
override suspend fun setSecondaryVolume(
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
volume: Int
@@ -262,6 +270,8 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
override val volumeMap: StateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
+ override suspend fun audioSharingAvailable(): Boolean = false
+
override suspend fun setSecondaryVolume(
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
volume: Int
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index b1489be943e6..22b315084f3f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -36,6 +37,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
+import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.TelephonyManager;
@@ -96,6 +98,8 @@ public class BluetoothEventManagerTest {
private LocalBluetoothProfileManager mLocalProfileManager;
@Mock
private BluetoothUtils.ErrorListener mErrorListener;
+ @Mock
+ private LocalBluetoothLeBroadcast mBroadcast;
private Context mContext;
private Intent mIntent;
@@ -107,7 +111,7 @@ public class BluetoothEventManagerTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
+ mContext = spy(RuntimeEnvironment.application);
mBluetoothEventManager =
new BluetoothEventManager(
@@ -208,28 +212,14 @@ public class BluetoothEventManagerTest {
*/
@Test
public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() {
- ShadowBluetoothAdapter shadowBluetoothAdapter =
- Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
- when(broadcast.isProfileReady()).thenReturn(true);
- LocalBluetoothLeBroadcastAssistant assistant =
- mock(LocalBluetoothLeBroadcastAssistant.class);
- when(assistant.isProfileReady()).thenReturn(true);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
+ setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
mCachedBluetoothDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -239,28 +229,14 @@ public class BluetoothEventManagerTest {
*/
@Test
public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() {
- ShadowBluetoothAdapter shadowBluetoothAdapter =
- Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
- BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
- shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
- when(broadcast.isProfileReady()).thenReturn(true);
- LocalBluetoothLeBroadcastAssistant assistant =
- mock(LocalBluetoothLeBroadcastAssistant.class);
- when(assistant.isProfileReady()).thenReturn(true);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */
+ true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
mCachedBluetoothDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -270,28 +246,14 @@ public class BluetoothEventManagerTest {
*/
@Test
public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() {
- ShadowBluetoothAdapter shadowBluetoothAdapter =
- Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
- when(broadcast.isProfileReady()).thenReturn(false);
- LocalBluetoothLeBroadcastAssistant assistant =
- mock(LocalBluetoothLeBroadcastAssistant.class);
- when(assistant.isProfileReady()).thenReturn(true);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ false, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
mCachedBluetoothDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -301,28 +263,31 @@ public class BluetoothEventManagerTest {
*/
@Test
public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() {
- ShadowBluetoothAdapter shadowBluetoothAdapter =
- Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
- when(broadcast.isProfileReady()).thenReturn(true);
- LocalBluetoothLeBroadcastAssistant assistant =
- mock(LocalBluetoothLeBroadcastAssistant.class);
- when(assistant.isProfileReady()).thenReturn(true);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
mCachedBluetoothDevice,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO);
- verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ }
+
+ /**
+ * dispatchProfileConnectionStateChanged should not call {@link
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for
+ * work profile.
+ */
+ @Test
+ public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() {
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ true);
+ mBluetoothEventManager.dispatchProfileConnectionStateChanged(
+ mCachedBluetoothDevice,
+ BluetoothProfile.STATE_DISCONNECTED,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+ verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -332,28 +297,40 @@ public class BluetoothEventManagerTest {
*/
@Test
public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ false);
+ mBluetoothEventManager.dispatchProfileConnectionStateChanged(
+ mCachedBluetoothDevice,
+ BluetoothProfile.STATE_DISCONNECTED,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+ verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ }
+
+ private void setUpAudioSharing(boolean enableFlag, boolean enableFeature,
+ boolean enableProfile, boolean workProfile) {
+ if (enableFlag) {
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ } else {
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ }
ShadowBluetoothAdapter shadowBluetoothAdapter =
Shadow.extract(BluetoothAdapter.getDefaultAdapter());
- shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
- BluetoothStatusCodes.FEATURE_SUPPORTED);
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
- LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
- when(broadcast.isProfileReady()).thenReturn(true);
+ int code = enableFeature ? BluetoothStatusCodes.FEATURE_SUPPORTED
+ : BluetoothStatusCodes.FEATURE_NOT_SUPPORTED;
+ shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(code);
+ shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(code);
+ when(mBroadcast.isProfileReady()).thenReturn(enableProfile);
LocalBluetoothLeBroadcastAssistant assistant =
mock(LocalBluetoothLeBroadcastAssistant.class);
- when(assistant.isProfileReady()).thenReturn(true);
+ when(assistant.isProfileReady()).thenReturn(enableProfile);
LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
+ when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
when(mBtManager.getProfileManager()).thenReturn(profileManager);
- mBluetoothEventManager.dispatchProfileConnectionStateChanged(
- mCachedBluetoothDevice,
- BluetoothProfile.STATE_DISCONNECTED,
- BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
-
- verify(broadcast).updateFallbackActiveDeviceIfNeeded();
+ UserManager userManager = mock(UserManager.class);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
+ when(userManager.isManagedProfile()).thenReturn(workProfile);
}
@Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
index b180b69f1e07..fd14d1ff6786 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
@@ -39,6 +39,7 @@ import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.os.Looper;
import android.os.Parcel;
+import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
import com.android.settingslib.flags.Flags;
@@ -104,6 +105,8 @@ public class CsipDeviceManagerTest {
private LocalBluetoothLeBroadcast mBroadcast;
@Mock
private LocalBluetoothLeBroadcastAssistant mAssistant;
+ @Mock
+ private UserManager mUserManager;
private ShadowBluetoothAdapter mShadowBluetoothAdapter;
private CachedBluetoothDevice mCachedDevice1;
@@ -128,7 +131,7 @@ public class CsipDeviceManagerTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
- mContext = RuntimeEnvironment.application;
+ mContext = spy(RuntimeEnvironment.application);
mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
mShadowBluetoothAdapter.setEnabled(true);
mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
@@ -356,6 +359,37 @@ public class CsipDeviceManagerTest {
}
@Test
+ public void
+ addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_workProfile_doNothing() {
+ // Condition: The preferredDevice is main and there is another main device in top list
+ // Expected Result: return true and there is the preferredDevice in top list
+ CachedBluetoothDevice preferredDevice = mCachedDevice1;
+ mCachedDevice1.getMemberDevice().clear();
+ mCachedDevices.clear();
+ mCachedDevices.add(preferredDevice);
+ mCachedDevices.add(mCachedDevice2);
+ mCachedDevices.add(mCachedDevice3);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+ when(mBroadcast.isEnabled(null)).thenReturn(true);
+ BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
+ when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
+ BluetoothLeBroadcastReceiveState state = Mockito.mock(
+ BluetoothLeBroadcastReceiveState.class);
+ when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
+ when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ when(mUserManager.isManagedProfile()).thenReturn(true);
+
+ assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
+ .isTrue();
+ assertThat(mCachedDevices.contains(preferredDevice)).isTrue();
+ assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse();
+ assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
+ assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
+ verify(mAssistant, never()).addSource(mDevice1, metadata, /* isGroupOp= */ false);
+ }
+
+ @Test
public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() {
// Condition: The preferredDevice is main and there is another main device in top list
// Expected Result: return true and there is the preferredDevice in top list
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
index 30e4637f59ff..6c1cb7015225 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
@@ -41,6 +41,9 @@ public class InputMediaDeviceTest {
private final int MAX_VOLUME = 1;
private final int CURRENT_VOLUME = 0;
private final boolean IS_VOLUME_FIXED = true;
+ private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
+ private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
+ private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -60,7 +63,8 @@ public class InputMediaDeviceTest {
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_BUILTIN_MIC);
assertThat(builtinMediaDevice).isNotNull();
assertThat(builtinMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_media_microphone);
}
@@ -74,7 +78,8 @@ public class InputMediaDeviceTest {
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_BUILTIN_MIC);
assertThat(builtinMediaDevice).isNotNull();
assertThat(builtinMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_this_device_name_desktop));
@@ -89,7 +94,8 @@ public class InputMediaDeviceTest {
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_WIRED_HEADSET);
assertThat(wiredMediaDevice).isNotNull();
assertThat(wiredMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_wired_device_mic_name));
@@ -104,7 +110,23 @@ public class InputMediaDeviceTest {
AudioDeviceInfo.TYPE_USB_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ PRODUCT_NAME_USB_HEADSET);
+ assertThat(usbMediaDevice).isNotNull();
+ assertThat(usbMediaDevice.getName()).isEqualTo(PRODUCT_NAME_USB_HEADSET);
+ }
+
+ @Test
+ public void getName_returnCorrectName_usbHeadset_nullProductName() {
+ InputMediaDevice usbMediaDevice =
+ InputMediaDevice.create(
+ mContext,
+ String.valueOf(USB_HEADSET_ID),
+ AudioDeviceInfo.TYPE_USB_HEADSET,
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ IS_VOLUME_FIXED,
+ null);
assertThat(usbMediaDevice).isNotNull();
assertThat(usbMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_usb_device_mic_name));
@@ -119,7 +141,8 @@ public class InputMediaDeviceTest {
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
MAX_VOLUME,
CURRENT_VOLUME,
- IS_VOLUME_FIXED);
+ IS_VOLUME_FIXED,
+ null);
assertThat(btMediaDevice).isNotNull();
assertThat(btMediaDevice.getName())
.isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name));
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index 29cc4034d4b4..782cee23fb42 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -21,6 +21,7 @@ import static com.android.settingslib.media.InputRouteManager.PRESETS;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -55,13 +56,95 @@ public class InputRouteManagerTest {
private static final int INPUT_USB_DEVICE_ID = 3;
private static final int INPUT_USB_HEADSET_ID = 4;
private static final int INPUT_USB_ACCESSORY_ID = 5;
+ private static final int HDMI_ID = 6;
private static final int MAX_VOLUME = 1;
private static final int CURRENT_VOLUME = 0;
private static final boolean VOLUME_FIXED_TRUE = true;
+ private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
+ private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
+ private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
+ private static final String PRODUCT_NAME_USB_DEVICE = "My USB Device";
+ private static final String PRODUCT_NAME_USB_ACCESSORY = "My USB Accessory";
+ private static final String PRODUCT_NAME_HDMI_DEVICE = "HDMI device";
private final Context mContext = spy(RuntimeEnvironment.application);
private InputRouteManager mInputRouteManager;
+ private AudioDeviceInfo mockBuiltinMicInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ when(info.getId()).thenReturn(BUILTIN_MIC_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
+ return info;
+ }
+
+ private AudioDeviceInfo mockWiredHeadsetInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
+ return info;
+ }
+
+ private AudioDeviceInfo mockUsbDeviceInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
+ when(info.getId()).thenReturn(INPUT_USB_DEVICE_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_DEVICE);
+ return info;
+ }
+
+ private AudioDeviceInfo mockUsbHeadsetInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
+ when(info.getId()).thenReturn(INPUT_USB_HEADSET_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_HEADSET);
+ return info;
+ }
+
+ private AudioDeviceInfo mockUsbAccessoryInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
+ when(info.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_ACCESSORY);
+ return info;
+ }
+
+ private AudioDeviceInfo mockHdmiInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
+ when(info.getId()).thenReturn(HDMI_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_HDMI_DEVICE);
+ return info;
+ }
+
+ private AudioDeviceAttributes getBuiltinMicDeviceAttributes() {
+ return new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_INPUT,
+ AudioDeviceInfo.TYPE_BUILTIN_MIC,
+ /* address= */ "");
+ }
+
+ private AudioDeviceAttributes getWiredHeadsetDeviceAttributes() {
+ return new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_INPUT,
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ /* address= */ "");
+ }
+
+ private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) {
+ final List<AudioDeviceAttributes> audioDeviceAttributesList =
+ new ArrayList<AudioDeviceAttributes>();
+ inputRouteManager.onPreferredDevicesForCapturePresetChangedListener(
+ MediaRecorder.AudioSource.MIC, audioDeviceAttributesList);
+ }
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -72,31 +155,15 @@ public class InputRouteManagerTest {
@Test
public void onAudioDevicesAdded_shouldUpdateInputMediaDevice() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info1.getId()).thenReturn(BUILTIN_MIC_ID);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-
- final AudioDeviceInfo info3 = mock(AudioDeviceInfo.class);
- when(info3.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
- when(info3.getId()).thenReturn(INPUT_USB_DEVICE_ID);
-
- final AudioDeviceInfo info4 = mock(AudioDeviceInfo.class);
- when(info4.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
- when(info4.getId()).thenReturn(INPUT_USB_HEADSET_ID);
-
- final AudioDeviceInfo info5 = mock(AudioDeviceInfo.class);
- when(info5.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
- when(info5.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
-
- final AudioDeviceInfo unsupportedInfo = mock(AudioDeviceInfo.class);
- when(unsupportedInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2, info3, info4, info5, unsupportedInfo};
+ AudioDeviceInfo[] devices = {
+ mockBuiltinMicInfo(),
+ mockWiredHeadsetInfo(),
+ mockUsbDeviceInfo(),
+ mockUsbHeadsetInfo(),
+ mockUsbAccessoryInfo(),
+ mockHdmiInfo()
+ };
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -104,8 +171,9 @@ public class InputRouteManagerTest {
assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
- // The unsupported info should be filtered out.
+ // The unsupported (hdmi) info should be filtered out.
assertThat(inputRouteManager.mInputMediaDevices).hasSize(devices.length - 1);
assertThat(inputRouteManager.mInputMediaDevices.get(0).getId())
.isEqualTo(String.valueOf(BUILTIN_MIC_ID));
@@ -130,34 +198,25 @@ public class InputRouteManagerTest {
final MediaDevice device = mock(MediaDevice.class);
inputRouteManager.mInputMediaDevices.add(device);
- final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
- when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(new AudioDeviceInfo[] {info});
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(
+ new AudioDeviceInfo[] {mockWiredHeadsetInfo()});
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
}
@Test
public void getSelectedInputDevice_returnOneFromAudioManager() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2};
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
// Mock audioManager.getDevicesForAttributes returns exactly one audioDeviceAttributes.
- AudioDeviceAttributes audioDeviceAttributes = new AudioDeviceAttributes(info1);
when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
- .thenReturn(Collections.singletonList(audioDeviceAttributes));
+ .thenReturn(Collections.singletonList(getWiredHeadsetDeviceAttributes()));
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
// The selected input device has the same type as the one returned from AudioManager.
InputMediaDevice selectedInputDevice =
@@ -168,29 +227,19 @@ public class InputRouteManagerTest {
@Test
public void getSelectedInputDevice_returnMoreThanOneFromAudioManager() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2};
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
// Mock audioManager.getDevicesForAttributes returns more than one audioDeviceAttributes.
- AudioDeviceAttributes audioDeviceAttributes1 = new AudioDeviceAttributes(info1);
- AudioDeviceAttributes audioDeviceAttributes2 = new AudioDeviceAttributes(info2);
List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
- attributesOfSelectedInputDevices.add(audioDeviceAttributes1);
- attributesOfSelectedInputDevices.add(audioDeviceAttributes2);
+ attributesOfSelectedInputDevices.add(getWiredHeadsetDeviceAttributes());
+ attributesOfSelectedInputDevices.add(getBuiltinMicDeviceAttributes());
when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
.thenReturn(attributesOfSelectedInputDevices);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
// The selected input device has the same type as the first one returned from AudioManager.
InputMediaDevice selectedInputDevice =
@@ -201,25 +250,17 @@ public class InputRouteManagerTest {
@Test
public void getSelectedInputDevice_returnEmptyFromAudioManager() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2};
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
// Mock audioManager.getDevicesForAttributes returns empty list of audioDeviceAttributes.
- List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+ List<AudioDeviceAttributes> emptyAttributesOfSelectedInputDevices = new ArrayList<>();
when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
- .thenReturn(attributesOfSelectedInputDevices);
+ .thenReturn(emptyAttributesOfSelectedInputDevices);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
// The selected input device has default type AudioDeviceInfo.TYPE_BUILTIN_MIC.
InputMediaDevice selectedInputDevice =
@@ -232,24 +273,66 @@ public class InputRouteManagerTest {
public void selectDevice() {
final AudioManager audioManager = mock(AudioManager.class);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- final MediaDevice inputMediaDevice =
+ final MediaDevice builtinMicDevice =
InputMediaDevice.create(
mContext,
String.valueOf(BUILTIN_MIC_ID),
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
- inputRouteManager.selectDevice(inputMediaDevice);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_BUILTIN_MIC);
+ inputRouteManager.selectDevice(builtinMicDevice);
- AudioDeviceAttributes deviceAttributes =
- new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_INPUT,
- AudioDeviceInfo.TYPE_BUILTIN_MIC,
- /* address= */ "");
for (@MediaRecorder.Source int preset : PRESETS) {
verify(audioManager, atLeastOnce())
- .setPreferredDeviceForCapturePreset(preset, deviceAttributes);
+ .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
+ }
+ }
+
+ @Test
+ public void onInitiation_shouldApplyDefaultSelectedDeviceToAllPresets() {
+ final AudioManager audioManager = mock(AudioManager.class);
+ new InputRouteManager(mContext, audioManager);
+
+ verify(audioManager, atLeastOnce()).getDevicesForAttributes(INPUT_ATTRIBUTES);
+ for (@MediaRecorder.Source int preset : PRESETS) {
+ verify(audioManager, atLeastOnce())
+ .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
+ }
+ }
+
+ @Test
+ public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() {
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes();
+ when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+ .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes));
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+ // Called twice, one after initiation, the other after onAudioDevicesAdded call.
+ verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+ for (@MediaRecorder.Source int preset : PRESETS) {
+ verify(audioManager, atLeast(2))
+ .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes);
+ }
+ }
+
+ @Test
+ public void onAudioDevicesRemoved_shouldApplyDefaultSelectedDeviceToAllPresets() {
+ final AudioManager audioManager = mock(AudioManager.class);
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(devices);
+
+ // Called twice, one after initiation, the other after onAudioDevicesRemoved call.
+ verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+ for (@MediaRecorder.Source int preset : PRESETS) {
+ verify(audioManager, atLeast(2))
+ .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
}
}
@@ -267,4 +350,56 @@ public class InputRouteManagerTest {
public void isInputGainFixed() {
assertThat(mInputRouteManager.isInputGainFixed()).isTrue();
}
+
+ @Test
+ public void onAudioDevicesAdded_shouldSetProductNameCorrectly() {
+ final AudioDeviceInfo info1 = mockWiredHeadsetInfo();
+ String firstProductName = "My first headset";
+ when(info1.getProductName()).thenReturn(firstProductName);
+ InputMediaDevice inputMediaDevice1 = createInputMediaDeviceFromDeviceInfo(info1);
+
+ final AudioDeviceInfo info2 = mockWiredHeadsetInfo();
+ String secondProductName = "My second headset";
+ when(info2.getProductName()).thenReturn(secondProductName);
+ InputMediaDevice inputMediaDevice2 = createInputMediaDeviceFromDeviceInfo(info2);
+
+ final AudioDeviceInfo infoWithNullProductName = mockWiredHeadsetInfo();
+ when(infoWithNullProductName.getProductName()).thenReturn(null);
+ InputMediaDevice inputMediaDevice3 =
+ createInputMediaDeviceFromDeviceInfo(infoWithNullProductName);
+
+ final AudioDeviceInfo infoWithBlankProductName = mockWiredHeadsetInfo();
+ when(infoWithBlankProductName.getProductName()).thenReturn("");
+ InputMediaDevice inputMediaDevice4 =
+ createInputMediaDeviceFromDeviceInfo(infoWithBlankProductName);
+
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceInfo[] devices = {
+ info1, info2, infoWithNullProductName, infoWithBlankProductName
+ };
+ when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+
+ assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
+
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
+
+ assertThat(inputRouteManager.mInputMediaDevices)
+ .containsExactly(
+ inputMediaDevice1, inputMediaDevice2, inputMediaDevice3, inputMediaDevice4)
+ .inOrder();
+ }
+
+ private InputMediaDevice createInputMediaDeviceFromDeviceInfo(AudioDeviceInfo info) {
+ return InputMediaDevice.create(
+ mContext,
+ String.valueOf(info.getId()),
+ info.getType(),
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ VOLUME_FIXED_TRUE,
+ info.getProductName() == null ? null : info.getProductName().toString());
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
index 529301138da3..d8b6707b9118 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
@@ -207,20 +207,6 @@ public class WifiUtilsTest {
}
@Test
- public void getHotspotIconResource_deviceTypeExists_shouldNotNull() {
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_PHONE))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_TABLET))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_LAPTOP))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_WATCH))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_AUTO))
- .isNotNull();
- }
-
- @Test
public void testInternetIconInjector_getIcon_returnsCorrectValues() {
WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext);
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 65b22758946d..1a99d25786ff 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -39,7 +39,6 @@ android_library {
"configinfra_framework_flags_java_lib",
"device_config_service_flags_java",
"libaconfig_java_proto_lite",
- "notification_flags_lib",
"SettingsLibDeviceStateRotationLock",
"SettingsLibDisplayUtils",
],
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 2cdd0aee3d85..3530e0f5f9de 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -106,6 +106,8 @@ public class SystemSettings {
Settings.System.UNREAD_NOTIFICATION_DOT_INDICATOR,
Settings.System.AUTO_LAUNCH_MEDIA_CONTROLS,
Settings.System.LOCALE_PREFERENCES,
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING,
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON,
Settings.System.TOUCHPAD_POINTER_SPEED,
Settings.System.TOUCHPAD_NATURAL_SCROLLING,
Settings.System.TOUCHPAD_TAP_TO_CLICK,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 282327739fc6..509b88b257fe 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -221,6 +221,8 @@ public class SystemSettingsValidators {
POINTER_ICON_VECTOR_STYLE_STROKE_END));
VALIDATORS.put(System.POINTER_SCALE,
new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE));
+ VALIDATORS.put(System.MOUSE_REVERSE_VERTICAL_SCROLLING, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(System.MOUSE_SWAP_PRIMARY_BUTTON, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index ec3bd90b91ea..6c3183191163 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -29,7 +29,6 @@ import android.hardware.display.ColorDisplayManager;
import android.icu.util.ULocale;
import android.media.AudioManager;
import android.media.RingtoneManager;
-import android.media.Utils;
import android.net.Uri;
import android.os.LocaleList;
import android.os.RemoteException;
@@ -310,13 +309,6 @@ public class SettingsHelper {
return SILENT_RINGTONE;
}
} else {
- // If the ringtone/notification support the vibration, use the original value.
- final int ringtoneType = getRingtoneType(name);
- if ((Settings.System.RINGTONE.equals(name)
- || Settings.System.NOTIFICATION_SOUND.equals(name))
- && hasVibrationSettings(value, ringtoneType)) {
- return value;
- }
return getCanonicalRingtoneValue(value);
}
}
@@ -370,15 +362,6 @@ public class SettingsHelper {
return;
}
- // If the ringtone/notification has vibration, we backup original value in onBackupValue.
- // So use the value directly for restoring.
- if ((ringtoneType == RingtoneManager.TYPE_RINGTONE
- || ringtoneType == RingtoneManager.TYPE_NOTIFICATION)
- && hasVibrationSettings(value, ringtoneType)) {
- RingtoneManager.setActualDefaultRingtoneUri(mContext, ringtoneType, Uri.parse(value));
- return;
- }
-
Uri ringtoneUri = null;
try {
ringtoneUri =
@@ -634,19 +617,6 @@ public class SettingsHelper {
return allLocales.remove(toFullLocale(filteredLocale));
}
- private boolean hasVibrationSettings(String value, int type) {
- if (Utils.hasVibration(Uri.parse(value)) && mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) {
- if (type == RingtoneManager.TYPE_RINGTONE) {
- return android.media.audio.Flags.enableRingtoneHapticsCustomization();
- }
- if (type == RingtoneManager.TYPE_NOTIFICATION) {
- return com.android.server.notification.Flags.notificationVibrationInSoundUri();
- }
- }
- return false;
- }
-
/**
* Sets the locale specified. Input data is the byte representation of comma separated
* multiple BCP-47 language tags. For backwards compatibility, strings of the form
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index babc1a37cc61..4b10b56f49fb 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -37,12 +37,9 @@ import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.media.AudioManager;
-import android.media.Utils;
import android.net.Uri;
import android.os.Bundle;
import android.os.LocaleList;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.provider.Settings;
@@ -57,7 +54,6 @@ import com.android.internal.R;
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -78,13 +74,9 @@ public class SettingsHelperTest {
"content://media/internal/audio/media/20?title=DefaultNotification&canonical=1";
private static final String DEFAULT_ALARM_VALUE =
"content://media/internal/audio/media/30?title=DefaultAlarm&canonical=1";
- private static final String VIBRATION_FILE_NAME = "haptics.xml";
private SettingsHelper mSettingsHelper;
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
@Mock private Context mContext;
@Mock private Resources mResources;
@Mock private AudioManager mAudioManager;
@@ -128,22 +120,6 @@ public class SettingsHelperTest {
}
@Test
- @EnableFlags({android.media.audio.Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION,
- com.android.server.notification.Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI})
- public void testOnBackupValue_ringtoneVibrationSupport_returnsSameValue() {
- when(mResources.getBoolean(
- com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)).thenReturn(
- true);
- String testRingtoneVibrationValue = createUriWithVibration(DEFAULT_RINGTONE_VALUE);
- String testNotificationVibrationValue = createUriWithVibration(DEFAULT_NOTIFICATION_VALUE);
-
- assertEquals(testRingtoneVibrationValue, mSettingsHelper.onBackupValue(
- Settings.System.RINGTONE, testRingtoneVibrationValue));
- assertEquals(testNotificationVibrationValue, mSettingsHelper.onBackupValue(
- Settings.System.NOTIFICATION_SOUND, testNotificationVibrationValue));
- }
-
- @Test
public void testGetRealValue_settingNotReplaced_returnsSameValue() {
when(mSettingsHelper.isReplacedSystemSetting(eq(SETTING_KEY))).thenReturn(false);
@@ -699,30 +675,6 @@ public class SettingsHelperTest {
.isEqualTo(null);
}
- @Test
- @EnableFlags({android.media.audio.Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION,
- com.android.server.notification.Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI})
- public void testRestoreValue_ringtoneVibrationSupport_restoreValue() {
- when(mResources.getBoolean(
- com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)).thenReturn(
- true);
- String testRingtoneVibrationValue = createUriWithVibration(DEFAULT_RINGTONE_VALUE);
- String testNotificationVibrationValue = createUriWithVibration(DEFAULT_NOTIFICATION_VALUE);
- ContentProvider mockMediaContentProvider =
- new MockContentProvider(mContext) {
- @Override
- public String getType(Uri url) {
- return "audio/ogg";
- }
- };
- mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
- resetRingtoneSettingsToDefault();
-
- assertRingtoneSettingsRestoring(Settings.System.RINGTONE, testRingtoneVibrationValue);
- assertRingtoneSettingsRestoring(
- Settings.System.NOTIFICATION_SOUND, testNotificationVibrationValue);
- }
-
private static class MockSettingsProvider extends MockContentProvider {
private final ArrayMap<String, String> mKeyValueStore = new ArrayMap<>();
MockSettingsProvider(Context context) {
@@ -814,25 +766,4 @@ public class SettingsHelperTest {
assertThat(Settings.System.getString(mContentResolver, Settings.System.ALARM_ALERT))
.isEqualTo(DEFAULT_ALARM_VALUE);
}
-
- private String createUriWithVibration(String defaultUriString) {
- return Uri.parse(defaultUriString).buildUpon()
- .appendQueryParameter(
- Utils.VIBRATION_URI_PARAM, VIBRATION_FILE_NAME).build().toString();
- }
-
- private void assertRingtoneSettingsRestoring(
- String settings, String testRingtoneSettingsValue) {
- mSettingsHelper.restoreValue(
- mContext,
- mContentResolver,
- new ContentValues(),
- Uri.EMPTY,
- settings,
- testRingtoneSettingsValue,
- 0);
-
- assertThat(Settings.System.getString(mContentResolver, settings))
- .isEqualTo(testRingtoneSettingsValue);
- }
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 456fedf912ff..408ed1e861c3 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -743,12 +743,6 @@
<uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" />
<uses-permission android:name="android.permission.MANAGE_SAFETY_CENTER" />
- <!-- Permissions required for CTS test - CtsVirtualDevicesTestCases -->
- <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
- <uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY" />
- <uses-permission android:name="android.permission.ADD_ALWAYS_UNLOCKED_DISPLAY" />
-
-
<!-- Permission required for CTS test - Notification test suite -->
<uses-permission android:name="android.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL" />
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1f10eadd115b..c4a45d06243b 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -425,8 +425,8 @@ filegroup {
"tests/src/**/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt",
"tests/src/**/systemui/shared/system/RemoteTransitionTest.java",
"tests/src/**/systemui/navigationbar/NavigationBarControllerImplTest.java",
- "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt",
- "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt",
+ "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt",
+ "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt",
"tests/src/**/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt",
"tests/src/**/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt",
"tests/src/**/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt",
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 07a1e630e1ad..380344a23c99 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -148,5 +148,10 @@
{
"name": "SystemUIGoogleRobo2RNGTests"
}
+ ],
+ "imports": [
+ {
+ "path": "cts/tests/tests/multiuser"
+ }
]
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index fc4cf1d1e21e..3dc0657f0d0d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -27,6 +27,8 @@ import android.util.Log
import android.util.MathUtils
import android.view.View
import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import android.view.ViewOverlay
import android.view.animation.Interpolator
import android.window.WindowAnimationState
import androidx.annotation.VisibleForTesting
@@ -197,10 +199,24 @@ class TransitionAnimator(
}
interface Animation {
+ /** Start the animation. */
+ fun start()
+
/** Cancel the animation. */
fun cancel()
}
+ @VisibleForTesting
+ class InterpolatedAnimation(@get:VisibleForTesting val animator: Animator) : Animation {
+ override fun start() {
+ animator.start()
+ }
+
+ override fun cancel() {
+ animator.cancel()
+ }
+ }
+
/** The timings (durations and delays) used by this animator. */
data class Timings(
/** The total duration of the animation. */
@@ -270,33 +286,73 @@ class TransitionAnimator(
alpha = 0
}
- val animator =
- createAnimator(
+ return createAnimation(
controller,
+ controller.createAnimatorState(),
endState,
windowBackgroundLayer,
fadeWindowBackgroundLayer,
drawHole,
)
- animator.start()
-
- return object : Animation {
- override fun cancel() {
- animator.cancel()
- }
- }
+ .apply { start() }
}
@VisibleForTesting
- fun createAnimator(
+ fun createAnimation(
controller: Controller,
+ startState: State,
endState: State,
windowBackgroundLayer: GradientDrawable,
fadeWindowBackgroundLayer: Boolean = true,
drawHole: Boolean = false,
- ): ValueAnimator {
- val state = controller.createAnimatorState()
+ ): Animation {
+ val transitionContainer = controller.transitionContainer
+ val transitionContainerOverlay = transitionContainer.overlay
+ val openingWindowSyncView = controller.openingWindowSyncView
+ val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
+ // Whether we should move the [windowBackgroundLayer] into the overlay of
+ // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or
+ // from it once the closing app window stops being visible.
+ // This is necessary as a one-off sync so we can avoid syncing at every frame, especially
+ // in complex interactions like launching an activity from a dialog. See
+ // b/214961273#comment2 for more details.
+ val moveBackgroundLayerWhenAppVisibilityChanges =
+ openingWindowSyncView != null &&
+ openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
+
+ return createInterpolatedAnimation(
+ controller,
+ startState,
+ endState,
+ windowBackgroundLayer,
+ transitionContainer,
+ transitionContainerOverlay,
+ openingWindowSyncView,
+ openingWindowSyncViewOverlay,
+ fadeWindowBackgroundLayer,
+ drawHole,
+ moveBackgroundLayerWhenAppVisibilityChanges,
+ )
+ }
+
+ /**
+ * Creates an interpolator-based animator that uses [timings] and [interpolators] to calculate
+ * the new bounds and corner radiuses at each frame.
+ */
+ private fun createInterpolatedAnimation(
+ controller: Controller,
+ state: State,
+ endState: State,
+ windowBackgroundLayer: GradientDrawable,
+ transitionContainer: View,
+ transitionContainerOverlay: ViewGroupOverlay,
+ openingWindowSyncView: View? = null,
+ openingWindowSyncViewOverlay: ViewOverlay? = null,
+ fadeWindowBackgroundLayer: Boolean = true,
+ drawHole: Boolean = false,
+ moveBackgroundLayerWhenAppVisibilityChanges: Boolean = false,
+ ): Animation {
// Start state.
val startTop = state.top
val startBottom = state.bottom
@@ -333,45 +389,24 @@ class TransitionAnimator(
}
}
- val transitionContainer = controller.transitionContainer
val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
+ var movedBackgroundLayer = false
// Update state.
val animator = ValueAnimator.ofFloat(0f, 1f)
animator.duration = timings.totalDuration
animator.interpolator = LINEAR
- // Whether we should move the [windowBackgroundLayer] into the overlay of
- // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or
- // from it once the closing app window stops being visible.
- // This is necessary as a one-off sync so we can avoid syncing at every frame, especially
- // in complex interactions like launching an activity from a dialog. See
- // b/214961273#comment2 for more details.
- val openingWindowSyncView = controller.openingWindowSyncView
- val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
- val moveBackgroundLayerWhenAppVisibilityChanges =
- openingWindowSyncView != null &&
- openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
-
- val transitionContainerOverlay = transitionContainer.overlay
- var movedBackgroundLayer = false
-
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
- if (DEBUG) {
- Log.d(TAG, "Animation started")
- }
- controller.onTransitionAnimationStart(isExpandingFullyAbove)
-
- // Add the drawable to the transition container overlay. Overlays always draw
- // drawables after views, so we know that it will be drawn above any view added
- // by the controller.
- if (controller.isLaunching || openingWindowSyncViewOverlay == null) {
- transitionContainerOverlay.add(windowBackgroundLayer)
- } else {
- openingWindowSyncViewOverlay.add(windowBackgroundLayer)
- }
+ onAnimationStart(
+ controller,
+ isExpandingFullyAbove,
+ windowBackgroundLayer,
+ transitionContainerOverlay,
+ openingWindowSyncViewOverlay,
+ )
}
override fun onAnimationEnd(animation: Animator) {
@@ -413,63 +448,20 @@ class TransitionAnimator(
state.bottomCornerRadius =
MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
- state.visible =
- if (controller.isLaunching) {
- // The expanding view can/should be hidden once it is completely covered by the
- // opening window.
- getProgress(
- timings,
- linearProgress,
- timings.contentBeforeFadeOutDelay,
- timings.contentBeforeFadeOutDuration,
- ) < 1
- } else {
- getProgress(
- timings,
- linearProgress,
- timings.contentAfterFadeInDelay,
- timings.contentAfterFadeInDuration,
- ) > 0
- }
-
- if (
- controller.isLaunching &&
- moveBackgroundLayerWhenAppVisibilityChanges &&
- !state.visible &&
- !movedBackgroundLayer
- ) {
- // The expanding view is not visible, so the opening app is visible. If this is
- // the first frame when it happens, trigger a one-off sync and move the
- // background layer in its new container.
- movedBackgroundLayer = true
-
- transitionContainerOverlay.remove(windowBackgroundLayer)
- openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
-
- ViewRootSync.synchronizeNextDraw(
- transitionContainer,
- openingWindowSyncView,
- then = {},
- )
- } else if (
- !controller.isLaunching &&
- moveBackgroundLayerWhenAppVisibilityChanges &&
- state.visible &&
- !movedBackgroundLayer
- ) {
- // The contracting view is now visible, so the closing app is not. If this is
- // the first frame when it happens, trigger a one-off sync and move the
- // background layer in its new container.
- movedBackgroundLayer = true
-
- openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
- transitionContainerOverlay.add(windowBackgroundLayer)
-
- ViewRootSync.synchronizeNextDraw(
- openingWindowSyncView,
- transitionContainer,
- then = {},
- )
+ state.visible = checkVisibility(timings, linearProgress, controller.isLaunching)
+
+ if (!movedBackgroundLayer) {
+ movedBackgroundLayer =
+ maybeMoveBackgroundLayer(
+ controller,
+ state,
+ windowBackgroundLayer,
+ transitionContainer,
+ transitionContainerOverlay,
+ openingWindowSyncView,
+ openingWindowSyncViewOverlay,
+ moveBackgroundLayerWhenAppVisibilityChanges,
+ )
}
val container =
@@ -478,7 +470,6 @@ class TransitionAnimator(
} else {
controller.transitionContainer
}
-
applyStateToWindowBackgroundLayer(
windowBackgroundLayer,
state,
@@ -488,10 +479,131 @@ class TransitionAnimator(
drawHole,
controller.isLaunching,
)
+
controller.onTransitionAnimationProgress(state, progress, linearProgress)
}
- return animator
+ return InterpolatedAnimation(animator)
+ }
+
+ private fun onAnimationStart(
+ controller: Controller,
+ isExpandingFullyAbove: Boolean,
+ windowBackgroundLayer: GradientDrawable,
+ transitionContainerOverlay: ViewGroupOverlay,
+ openingWindowSyncViewOverlay: ViewOverlay?,
+ ) {
+ if (DEBUG) {
+ Log.d(TAG, "Animation started")
+ }
+ controller.onTransitionAnimationStart(isExpandingFullyAbove)
+
+ // Add the drawable to the transition container overlay. Overlays always draw
+ // drawables after views, so we know that it will be drawn above any view added
+ // by the controller.
+ if (controller.isLaunching || openingWindowSyncViewOverlay == null) {
+ transitionContainerOverlay.add(windowBackgroundLayer)
+ } else {
+ openingWindowSyncViewOverlay.add(windowBackgroundLayer)
+ }
+ }
+
+ private fun onAnimationEnd(
+ controller: Controller,
+ isExpandingFullyAbove: Boolean,
+ windowBackgroundLayer: GradientDrawable,
+ transitionContainerOverlay: ViewGroupOverlay,
+ openingWindowSyncViewOverlay: ViewOverlay?,
+ moveBackgroundLayerWhenAppVisibilityChanges: Boolean,
+ ) {
+ if (DEBUG) {
+ Log.d(TAG, "Animation ended")
+ }
+
+ // TODO(b/330672236): Post this to the main thread instead so that it does not
+ // flicker with Flexiglass enabled.
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
+
+ if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+ openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+ }
+ }
+
+ /** Returns whether is the controller's view should be visible with the given [timings]. */
+ private fun checkVisibility(timings: Timings, progress: Float, isLaunching: Boolean): Boolean {
+ return if (isLaunching) {
+ // The expanding view can/should be hidden once it is completely covered by the opening
+ // window.
+ getProgress(
+ timings,
+ progress,
+ timings.contentBeforeFadeOutDelay,
+ timings.contentBeforeFadeOutDuration,
+ ) < 1
+ } else {
+ // The shrinking view can/should be hidden while it is completely covered by the closing
+ // window.
+ getProgress(
+ timings,
+ progress,
+ timings.contentAfterFadeInDelay,
+ timings.contentAfterFadeInDuration,
+ ) > 0
+ }
+ }
+
+ /**
+ * If necessary, moves the background layer from the view container's overlay to the window sync
+ * view overlay, or vice versa.
+ *
+ * @return true if the background layer vwas moved, false otherwise.
+ */
+ private fun maybeMoveBackgroundLayer(
+ controller: Controller,
+ state: State,
+ windowBackgroundLayer: GradientDrawable,
+ transitionContainer: View,
+ transitionContainerOverlay: ViewGroupOverlay,
+ openingWindowSyncView: View?,
+ openingWindowSyncViewOverlay: ViewOverlay?,
+ moveBackgroundLayerWhenAppVisibilityChanges: Boolean,
+ ): Boolean {
+ if (
+ controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && !state.visible
+ ) {
+ // The expanding view is not visible, so the opening app is visible. If this is the
+ // first frame when it happens, trigger a one-off sync and move the background layer
+ // in its new container.
+ transitionContainerOverlay.remove(windowBackgroundLayer)
+ openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
+
+ ViewRootSync.synchronizeNextDraw(
+ transitionContainer,
+ openingWindowSyncView!!,
+ then = {},
+ )
+
+ return true
+ } else if (
+ !controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && state.visible
+ ) {
+ // The contracting view is now visible, so the closing app is not. If this is the first
+ // frame when it happens, trigger a one-off sync and move the background layer in its
+ // new container.
+ openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
+ transitionContainerOverlay.add(windowBackgroundLayer)
+
+ ViewRootSync.synchronizeNextDraw(
+ openingWindowSyncView!!,
+ transitionContainer,
+ then = {},
+ )
+
+ return true
+ }
+
+ return false
}
/** Return whether we are expanding fully above the [transitionContainer]. */
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
index 37c37b01fc03..6b3223df9532 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
@@ -16,8 +16,8 @@
package com.android.compose.theme
-import android.annotation.ColorInt
import android.content.Context
+import androidx.annotation.ColorRes
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import com.android.internal.R
@@ -34,62 +34,27 @@ val LocalAndroidColorScheme =
/**
* The Android color scheme.
*
- * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future,
- * most of the colors in this class will be removed in favor of their M3 counterpart.
+ * This scheme contains the Material3 colors that are not available on
+ * [androidx.compose.material3.MaterialTheme]. For other colors (e.g. primary), use
+ * `MaterialTheme.colorScheme` instead.
*/
-class AndroidColorScheme(context: Context) {
- val onSecondaryFixedVariant = getColor(context, R.attr.materialColorOnSecondaryFixedVariant)
- val onTertiaryFixedVariant = getColor(context, R.attr.materialColorOnTertiaryFixedVariant)
- val surfaceContainerLowest = getColor(context, R.attr.materialColorSurfaceContainerLowest)
- val onPrimaryFixedVariant = getColor(context, R.attr.materialColorOnPrimaryFixedVariant)
- val onSecondaryContainer = getColor(context, R.attr.materialColorOnSecondaryContainer)
- val onTertiaryContainer = getColor(context, R.attr.materialColorOnTertiaryContainer)
- val surfaceContainerLow = getColor(context, R.attr.materialColorSurfaceContainerLow)
- val onPrimaryContainer = getColor(context, R.attr.materialColorOnPrimaryContainer)
- val secondaryFixedDim = getColor(context, R.attr.materialColorSecondaryFixedDim)
- val onErrorContainer = getColor(context, R.attr.materialColorOnErrorContainer)
- val onSecondaryFixed = getColor(context, R.attr.materialColorOnSecondaryFixed)
- val onSurfaceInverse = getColor(context, R.attr.materialColorOnSurfaceInverse)
- val tertiaryFixedDim = getColor(context, R.attr.materialColorTertiaryFixedDim)
- val onTertiaryFixed = getColor(context, R.attr.materialColorOnTertiaryFixed)
- val primaryFixedDim = getColor(context, R.attr.materialColorPrimaryFixedDim)
- val secondaryContainer = getColor(context, R.attr.materialColorSecondaryContainer)
- val errorContainer = getColor(context, R.attr.materialColorErrorContainer)
- val onPrimaryFixed = getColor(context, R.attr.materialColorOnPrimaryFixed)
- val primaryInverse = getColor(context, R.attr.materialColorPrimaryInverse)
- val secondaryFixed = getColor(context, R.attr.materialColorSecondaryFixed)
- val surfaceInverse = getColor(context, R.attr.materialColorSurfaceInverse)
- val surfaceVariant = getColor(context, R.attr.materialColorSurfaceVariant)
- val tertiaryContainer = getColor(context, R.attr.materialColorTertiaryContainer)
- val tertiaryFixed = getColor(context, R.attr.materialColorTertiaryFixed)
- val primaryContainer = getColor(context, R.attr.materialColorPrimaryContainer)
- val onBackground = getColor(context, R.attr.materialColorOnBackground)
- val primaryFixed = getColor(context, R.attr.materialColorPrimaryFixed)
- val onSecondary = getColor(context, R.attr.materialColorOnSecondary)
- val onTertiary = getColor(context, R.attr.materialColorOnTertiary)
- val surfaceDim = getColor(context, R.attr.materialColorSurfaceDim)
- val surfaceBright = getColor(context, R.attr.materialColorSurfaceBright)
- val error = getColor(context, R.attr.materialColorError)
- val onError = getColor(context, R.attr.materialColorOnError)
- val surface = getColor(context, R.attr.materialColorSurface)
- val surfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh)
- val surfaceContainerHighest = getColor(context, R.attr.materialColorSurfaceContainerHighest)
- val onSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant)
- val outline = getColor(context, R.attr.materialColorOutline)
- val outlineVariant = getColor(context, R.attr.materialColorOutlineVariant)
- val onPrimary = getColor(context, R.attr.materialColorOnPrimary)
- val onSurface = getColor(context, R.attr.materialColorOnSurface)
- val surfaceContainer = getColor(context, R.attr.materialColorSurfaceContainer)
- val primary = getColor(context, R.attr.materialColorPrimary)
- val secondary = getColor(context, R.attr.materialColorSecondary)
- val tertiary = getColor(context, R.attr.materialColorTertiary)
+class AndroidColorScheme(val context: Context) {
+ val primaryFixed = color(context, R.color.system_primary_fixed)
+ val primaryFixedDim = color(context, R.color.system_primary_fixed_dim)
+ val onPrimaryFixed = color(context, R.color.system_on_primary_fixed)
+ val onPrimaryFixedVariant = color(context, R.color.system_on_primary_fixed_variant)
+ val secondaryFixed = color(context, R.color.system_secondary_fixed)
+ val secondaryFixedDim = color(context, R.color.system_secondary_fixed_dim)
+ val onSecondaryFixed = color(context, R.color.system_on_secondary_fixed)
+ val onSecondaryFixedVariant = color(context, R.color.system_on_secondary_fixed_variant)
+ val tertiaryFixed = color(context, R.color.system_tertiary_fixed)
+ val tertiaryFixedDim = color(context, R.color.system_tertiary_fixed_dim)
+ val onTertiaryFixed = color(context, R.color.system_on_tertiary_fixed)
+ val onTertiaryFixedVariant = color(context, R.color.system_on_tertiary_fixed_variant)
companion object {
- internal fun getColor(context: Context, attr: Int): Color {
- val ta = context.obtainStyledAttributes(intArrayOf(attr))
- @ColorInt val color = ta.getColor(0, 0)
- ta.recycle()
- return Color(color)
+ internal fun color(context: Context, @ColorRes id: Int): Color {
+ return Color(context.resources.getColor(id, context.theme))
}
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/Color.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/Color.kt
index 5dbaff650a93..a499447fc367 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/Color.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/Color.kt
@@ -17,6 +17,8 @@
package com.android.compose.theme
import android.annotation.AttrRes
+import android.annotation.ColorInt
+import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
@@ -26,5 +28,13 @@ import androidx.compose.ui.platform.LocalContext
@Composable
@ReadOnlyComposable
fun colorAttr(@AttrRes attribute: Int): Color {
- return AndroidColorScheme.getColor(LocalContext.current, attribute)
+ return colorAttr(LocalContext.current, attribute)
+}
+
+/** Return the [Color] from the given [attribute]. */
+fun colorAttr(context: Context, @AttrRes attr: Int): Color {
+ val ta = context.obtainStyledAttributes(intArrayOf(attr))
+ @ColorInt val color = ta.getColor(0, 0)
+ ta.recycle()
+ return Color(color)
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
index 06618704e085..d31d7aa59489 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
@@ -16,7 +16,9 @@
package com.android.compose.theme
+import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@@ -24,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
+import com.android.compose.theme.AndroidColorScheme.Companion.color
import com.android.compose.theme.typography.TypeScaleTokens
import com.android.compose.theme.typography.TypefaceNames
import com.android.compose.theme.typography.TypefaceTokens
@@ -31,23 +34,15 @@ import com.android.compose.theme.typography.TypographyTokens
import com.android.compose.theme.typography.platformTypography
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.compose.windowsizeclass.calculateWindowSizeClass
+import com.android.internal.R
/** The Material 3 theme that should wrap all Platform Composables. */
@Composable
-fun PlatformTheme(
- isDarkTheme: Boolean = isSystemInDarkTheme(),
- content: @Composable () -> Unit,
-) {
+fun PlatformTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val context = LocalContext.current
- // TODO(b/230605885): Define our color scheme.
- val colorScheme =
- if (isDarkTheme) {
- dynamicDarkColorScheme(context)
- } else {
- dynamicLightColorScheme(context)
- }
- val androidColorScheme = AndroidColorScheme(context)
+ val colorScheme = remember(context, isDarkTheme) { platformColorScheme(isDarkTheme, context) }
+ val androidColorScheme = remember(context) { AndroidColorScheme(context) }
val typefaceNames = remember(context) { TypefaceNames.get(context) }
val typography =
remember(typefaceNames) {
@@ -55,12 +50,31 @@ fun PlatformTheme(
}
val windowSizeClass = calculateWindowSizeClass()
- MaterialTheme(colorScheme, typography = typography) {
+ MaterialTheme(colorScheme = colorScheme, typography = typography) {
CompositionLocalProvider(
LocalAndroidColorScheme provides androidColorScheme,
LocalWindowSizeClass provides windowSizeClass,
- ) {
- content()
- }
+ content = content,
+ )
+ }
+}
+
+private fun platformColorScheme(isDarkTheme: Boolean, context: Context): ColorScheme {
+ return if (isDarkTheme) {
+ dynamicDarkColorScheme(context)
+ .copy(
+ error = color(context, R.color.system_error_dark),
+ onError = color(context, R.color.system_on_error_dark),
+ errorContainer = color(context, R.color.system_error_container_dark),
+ onErrorContainer = color(context, R.color.system_on_error_container_dark),
+ )
+ } else {
+ dynamicLightColorScheme(context)
+ .copy(
+ error = color(context, R.color.system_error_light),
+ onError = color(context, R.color.system_on_error_light),
+ errorContainer = color(context, R.color.system_error_container_light),
+ onErrorContainer = color(context, R.color.system_on_error_container_light),
+ )
}
}
diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp
index 6e7a1425ef90..6a824d8d30f0 100644
--- a/packages/SystemUI/compose/core/tests/Android.bp
+++ b/packages/SystemUI/compose/core/tests/Android.bp
@@ -27,7 +27,6 @@ android_test {
name: "PlatformComposeCoreTests",
manifest: "AndroidManifest.xml",
test_suites: ["device-tests"],
- sdk_version: "current",
certificate: "platform",
srcs: [
diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
index 1016340a7171..28f80d4af265 100644
--- a/packages/SystemUI/compose/core/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml
@@ -19,6 +19,11 @@
<application>
<uses-library android:name="android.test.runner" />
+
+ <activity
+ android:name="androidx.activity.ComponentActivity"
+ android:theme="@android:style/Theme.DeviceDefault.DayNight"
+ android:exported="true" />
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/PlatformThemeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/PlatformThemeTest.kt
index 23538e3fb702..de021a0677cf 100644
--- a/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/PlatformThemeTest.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/theme/PlatformThemeTest.kt
@@ -16,11 +16,21 @@
package com.android.compose.theme
+import android.content.Context
+import androidx.annotation.AttrRes
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.internal.R
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
@@ -54,4 +64,145 @@ class PlatformThemeTest {
}
}
}
+
+ @Test
+ fun testMaterialColorsMatchAttributeValue() {
+ val colorValues = mutableListOf<ColorValue>()
+
+ fun onLaunch(colorScheme: ColorScheme, context: Context) {
+ fun addValue(name: String, materialValue: Color, @AttrRes attr: Int) {
+ colorValues.add(ColorValue(name, materialValue, colorAttr(context, attr)))
+ }
+
+ addValue("primary", colorScheme.primary, R.attr.materialColorPrimary)
+ addValue("onPrimary", colorScheme.onPrimary, R.attr.materialColorOnPrimary)
+ addValue(
+ "primaryContainer",
+ colorScheme.primaryContainer,
+ R.attr.materialColorPrimaryContainer,
+ )
+ addValue(
+ "onPrimaryContainer",
+ colorScheme.onPrimaryContainer,
+ R.attr.materialColorOnPrimaryContainer,
+ )
+ addValue(
+ "inversePrimary",
+ colorScheme.inversePrimary,
+ R.attr.materialColorPrimaryInverse,
+ )
+ addValue("secondary", colorScheme.secondary, R.attr.materialColorSecondary)
+ addValue("onSecondary", colorScheme.onSecondary, R.attr.materialColorOnSecondary)
+ addValue(
+ "secondaryContainer",
+ colorScheme.secondaryContainer,
+ R.attr.materialColorSecondaryContainer,
+ )
+ addValue(
+ "onSecondaryContainer",
+ colorScheme.onSecondaryContainer,
+ R.attr.materialColorOnSecondaryContainer,
+ )
+ addValue("tertiary", colorScheme.tertiary, R.attr.materialColorTertiary)
+ addValue("onTertiary", colorScheme.onTertiary, R.attr.materialColorOnTertiary)
+ addValue(
+ "tertiaryContainer",
+ colorScheme.tertiaryContainer,
+ R.attr.materialColorTertiaryContainer,
+ )
+ addValue(
+ "onTertiaryContainer",
+ colorScheme.onTertiaryContainer,
+ R.attr.materialColorOnTertiaryContainer,
+ )
+ addValue("onBackground", colorScheme.onBackground, R.attr.materialColorOnBackground)
+ addValue("surface", colorScheme.surface, R.attr.materialColorSurface)
+ addValue("onSurface", colorScheme.onSurface, R.attr.materialColorOnSurface)
+ addValue(
+ "surfaceVariant",
+ colorScheme.surfaceVariant,
+ R.attr.materialColorSurfaceVariant,
+ )
+ addValue(
+ "onSurfaceVariant",
+ colorScheme.onSurfaceVariant,
+ R.attr.materialColorOnSurfaceVariant,
+ )
+ addValue(
+ "inverseSurface",
+ colorScheme.inverseSurface,
+ R.attr.materialColorSurfaceInverse,
+ )
+ addValue(
+ "inverseOnSurface",
+ colorScheme.inverseOnSurface,
+ R.attr.materialColorOnSurfaceInverse,
+ )
+ addValue("error", colorScheme.error, R.attr.materialColorError)
+ addValue("onError", colorScheme.onError, R.attr.materialColorOnError)
+ addValue(
+ "errorContainer",
+ colorScheme.errorContainer,
+ R.attr.materialColorErrorContainer,
+ )
+ addValue(
+ "onErrorContainer",
+ colorScheme.onErrorContainer,
+ R.attr.materialColorOnErrorContainer,
+ )
+ addValue("outline", colorScheme.outline, R.attr.materialColorOutline)
+ addValue(
+ "outlineVariant",
+ colorScheme.outlineVariant,
+ R.attr.materialColorOutlineVariant,
+ )
+ addValue("surfaceBright", colorScheme.surfaceBright, R.attr.materialColorSurfaceBright)
+ addValue("surfaceDim", colorScheme.surfaceDim, R.attr.materialColorSurfaceDim)
+ addValue(
+ "surfaceContainer",
+ colorScheme.surfaceContainer,
+ R.attr.materialColorSurfaceContainer,
+ )
+ addValue(
+ "surfaceContainerHigh",
+ colorScheme.surfaceContainerHigh,
+ R.attr.materialColorSurfaceContainerHigh,
+ )
+ addValue(
+ "surfaceContainerHighest",
+ colorScheme.surfaceContainerHighest,
+ R.attr.materialColorSurfaceContainerHighest,
+ )
+ addValue(
+ "surfaceContainerLow",
+ colorScheme.surfaceContainerLow,
+ R.attr.materialColorSurfaceContainerLow,
+ )
+ addValue(
+ "surfaceContainerLowest",
+ colorScheme.surfaceContainerLowest,
+ R.attr.materialColorSurfaceContainerLowest,
+ )
+ }
+
+ composeRule.setContent {
+ PlatformTheme {
+ val colorScheme = MaterialTheme.colorScheme
+ val context = LocalContext.current
+
+ LaunchedEffect(Unit) { onLaunch(colorScheme, context) }
+ }
+ }
+
+ assertThat(colorValues).hasSize(33)
+ colorValues.forEach { colorValue ->
+ assertWithMessage(
+ "MaterialTheme.colorScheme.${colorValue.name} matches attribute color"
+ )
+ .that(colorValue.materialValue)
+ .isEqualTo(colorValue.attrValue)
+ }
+ }
+
+ private data class ColorValue(val name: String, val materialValue: Color, val attrValue: Color)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 571b36639c9e..6d3039855077 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -13,6 +13,7 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -44,8 +45,6 @@ import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.transitions
-import com.android.compose.theme.LocalAndroidColorScheme
-import com.android.internal.R.attr.focusable
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.communal.shared.model.CommunalTransitionKeys
@@ -270,7 +269,7 @@ private fun BoxScope.DefaultBackground(
/** Experimental hub background, static linear gradient */
@Composable
private fun BoxScope.StaticLinearGradient() {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Box(
Modifier.matchParentSize()
.background(
@@ -283,7 +282,7 @@ private fun BoxScope.StaticLinearGradient() {
/** Experimental hub background, animated linear gradient */
@Composable
private fun BoxScope.AnimatedLinearGradient() {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Box(
Modifier.matchParentSize()
.background(colors.primary)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt
index 6fca1785d768..9392b1afffa3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt
@@ -19,12 +19,12 @@ package com.android.systemui.communal.ui.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.IntRect
import com.android.compose.animation.scene.SceneScope
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler
import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection
import com.android.systemui.communal.ui.compose.section.CommunalPopupSection
@@ -71,7 +71,7 @@ constructor(
}
with(lockSection) {
LockIcon(
- overrideColor = LocalAndroidColorScheme.current.onPrimaryContainer,
+ overrideColor = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.element(Communal.Elements.LockIcon)
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 7fb4c537641b..96c47cc5fb6f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -161,7 +161,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.layout.WindowMetricsCalculator
import com.android.compose.animation.Easings.Emphasized
import com.android.compose.modifiers.thenIf
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.internal.R.dimen.system_app_widget_background_radius
import com.android.systemui.Flags
@@ -473,7 +472,7 @@ fun CommunalHub(
if (showBottomSheet) {
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState()
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
ModalBottomSheet(
onDismissRequest = viewModel::onDisclaimerDismissed,
@@ -501,7 +500,7 @@ val hubDimensions: Dimensions
@Composable
private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Column(
modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp, vertical = 24.dp),
@@ -817,7 +816,7 @@ private fun BoxScope.CommunalHubLazyGrid(
*/
@Composable
private fun EmptyStateCta(contentPadding: PaddingValues, viewModel: BaseCommunalViewModel) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Card(
modifier = Modifier.height(hubDimensions.GridHeight).padding(contentPadding),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
@@ -963,7 +962,7 @@ private fun ToolbarButton(
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
AnimatedVisibility(
visible = isPrimary,
modifier = modifier,
@@ -1010,7 +1009,7 @@ private fun ToolbarButton(
@Composable
private fun filledButtonColors(): ButtonColors {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
return ButtonDefaults.buttonColors(
containerColor = colors.primary,
contentColor = colors.onPrimary,
@@ -1058,7 +1057,7 @@ private fun CommunalContent(
/** Creates an empty card used to highlight a particular spot on the grid. */
@Composable
fun HighlightedItem(modifier: Modifier = Modifier, alpha: Float = 1.0f) {
- val brush = SolidColor(LocalAndroidColorScheme.current.primary)
+ val brush = SolidColor(MaterialTheme.colorScheme.primary)
Box(
modifier =
// drawBehind lets us draw outside the bounds of the widgets so that we don't need to
@@ -1085,7 +1084,7 @@ private fun CtaTileInViewModeContent(
viewModel: BaseCommunalViewModel,
modifier: Modifier = Modifier,
) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Card(
modifier = modifier,
colors =
@@ -1301,7 +1300,7 @@ fun WidgetConfigureButton(
modifier: Modifier = Modifier,
widgetConfigurator: WidgetConfigurator,
) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
val scope = rememberCoroutineScope()
AnimatedVisibility(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/EnableWidgetDialog.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/EnableWidgetDialog.kt
index df11206826b8..b2407fa33f70 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/EnableWidgetDialog.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/EnableWidgetDialog.kt
@@ -41,7 +41,6 @@ import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
@@ -93,7 +92,7 @@ private fun DialogComposable(
Box(
Modifier.fillMaxWidth()
.padding(top = 18.dp, bottom = 8.dp)
- .background(LocalAndroidColorScheme.current.surfaceBright, RoundedCornerShape(28.dp))
+ .background(MaterialTheme.colorScheme.surfaceBright, RoundedCornerShape(28.dp))
) {
Column(
modifier = Modifier.fillMaxWidth(),
@@ -106,7 +105,7 @@ private fun DialogComposable(
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
- color = LocalAndroidColorScheme.current.onSurface,
+ color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 1,
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
index ef62eb726e2b..97ad4f127c98 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -47,7 +48,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastIsFinite
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.ui.viewmodel.DragHandle
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
@@ -169,7 +169,7 @@ fun ResizableItemFrame(
modifier: Modifier = Modifier,
enabled: Boolean = true,
outlinePadding: Dp = 8.dp,
- outlineColor: Color = LocalAndroidColorScheme.current.primary,
+ outlineColor: Color = MaterialTheme.colorScheme.primary,
cornerRadius: Dp = 37.dp,
strokeWidth: Dp = 3.dp,
alpha: () -> Float = { 1f },
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
index b4c1a2e85daf..868e136dbd85 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
@@ -55,7 +55,6 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.ui.viewmodel.PopupType
import com.android.systemui.res.R
@@ -112,7 +111,7 @@ constructor(
offset = IntOffset(0, 40),
onDismissRequest = onDismissRequest,
) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Button(
modifier =
Modifier.height(56.dp)
@@ -182,7 +181,7 @@ constructor(
offset = IntOffset(0, 40),
onDismissRequest = onDismissRequest
) {
- val colors = LocalAndroidColorScheme.current
+ val colors = MaterialTheme.colorScheme
Row(
modifier =
Modifier.height(56.dp)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index 3d8ca1e96a09..b5d78398028d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -34,7 +34,6 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.MovableElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.compose.windowsizeclass.LocalWindowSizeClass
-import com.android.internal.R.attr.layout
import com.android.systemui.media.controls.ui.composable.MediaCarouselStateLoader.stateForMediaCarouselContent
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
import com.android.systemui.media.controls.ui.view.MediaHost
@@ -60,12 +59,12 @@ fun SceneScope.MediaCarousel(
carouselController: MediaCarouselController,
offsetProvider: (() -> IntOffset)? = null,
usingCollapsedLandscapeMedia: Boolean = false,
+ isInSplitShade: Boolean = false,
) {
if (!isVisible || carouselController.isLockedAndHidden()) {
return
}
-
- val carouselState = remember { { stateForMediaCarouselContent() } }
+ val carouselState = remember { { stateForMediaCarouselContent(isInSplitShade) } }
val isCollapsed = usingCollapsedLandscapeMedia && isLandscape()
val mediaHeight =
if (isCollapsed && mediaHost.expansion == MediaHostState.COLLAPSED) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarouselStateLoader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarouselStateLoader.kt
index 4a0136c40c14..bad74052b669 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarouselStateLoader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarouselStateLoader.kt
@@ -43,10 +43,13 @@ object MediaCarouselStateLoader {
/** Returns the corresponding media location for the given [scene] */
@MediaLocation
- private fun getMediaLocation(scene: SceneKey): Int {
+ private fun getMediaLocation(scene: SceneKey, isSplitShade: Boolean): Int {
return when (scene) {
Scenes.QuickSettings -> MediaHierarchyManager.LOCATION_QS
- Scenes.Shade -> MediaHierarchyManager.LOCATION_QQS
+ Scenes.Shade -> {
+ if (isSplitShade) MediaHierarchyManager.LOCATION_QS
+ else MediaHierarchyManager.LOCATION_QQS
+ }
Scenes.Lockscreen -> MediaHierarchyManager.LOCATION_LOCKSCREEN
Scenes.Communal -> MediaHierarchyManager.LOCATION_COMMUNAL_HUB
else -> MediaHierarchyManager.LOCATION_UNKNOWN
@@ -97,11 +100,11 @@ object MediaCarouselStateLoader {
}
/** Returns the state of media carousel */
- fun SceneScope.stateForMediaCarouselContent(): State {
+ fun SceneScope.stateForMediaCarouselContent(isInSplitShade: Boolean): State {
return when (val transitionState = layoutState.transitionState) {
is TransitionState.Idle -> {
if (MediaContentPicker.contents.contains(transitionState.currentScene)) {
- State.Idle(getMediaLocation(transitionState.currentScene))
+ State.Idle(getMediaLocation(transitionState.currentScene, isInSplitShade))
} else {
State.Gone
}
@@ -114,14 +117,14 @@ object MediaCarouselStateLoader {
) {
State.InProgress(
min(progress, 1.0F),
- getMediaLocation(fromScene),
- getMediaLocation(toScene),
+ getMediaLocation(fromScene, isInSplitShade),
+ getMediaLocation(toScene, isInSplitShade),
)
} else if (MediaContentPicker.contents.contains(toScene)) {
State.InProgress(
transitionProgress = 1.0F,
startLocation = MediaHierarchyManager.LOCATION_UNKNOWN,
- getMediaLocation(toScene),
+ getMediaLocation(toScene, isInSplitShade),
)
} else {
State.Gone
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index e8da4bd2d2cd..e382e164d2d4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -73,7 +73,6 @@ import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.animation.Expandable
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.background
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.theme.colorAttr
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
@@ -163,7 +162,7 @@ fun FooterActions(
}
val backgroundColor = colorAttr(R.attr.underSurface)
- val contentColor = LocalAndroidColorScheme.current.onSurface
+ val contentColor = MaterialTheme.colorScheme.onSurface
val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
val backgroundModifier =
remember(
@@ -344,7 +343,7 @@ private fun NumberButton(
@Composable
private fun NewChangesDot(modifier: Modifier = Modifier) {
val contentDescription = stringResource(R.string.fgs_dot_content_description)
- val color = LocalAndroidColorScheme.current.tertiary
+ val color = MaterialTheme.colorScheme.tertiary
Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
drawCircle(color)
@@ -363,7 +362,7 @@ private fun TextButton(
Expandable(
shape = CircleShape,
color = colorAttr(R.attr.underSurface),
- contentColor = LocalAndroidColorScheme.current.onSurfaceVariant,
+ contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
borderStroke = BorderStroke(1.dp, colorAttr(R.attr.shadeInactive)),
modifier = modifier.padding(horizontal = 4.dp),
onClick = onClick,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index edc4cba2d53f..58801e01d9d6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -24,7 +24,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.layout.layout
@@ -38,6 +40,7 @@ import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.MovableElementContentPicker
import com.android.compose.animation.scene.MovableElementKey
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.ValueKey
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
@@ -158,12 +161,10 @@ fun SceneScope.QuickSettings(
squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
) {
val contentState = { stateForQuickSettingsContent(isSplitShade, squishiness) }
- val transitionState = layoutState.transitionState
- val isClosing =
- transitionState is TransitionState.Transition &&
- transitionState.progress >= 0.9f && // almost done closing
- !(layoutState.isTransitioning(to = Scenes.Shade) ||
- layoutState.isTransitioning(to = Scenes.QuickSettings))
+
+ // Note: We use derivedStateOf {} here because isClosing() is reading the current transition
+ // progress and we don't want to recompose this scene each time the progress has changed.
+ val isClosing by remember(layoutState) { derivedStateOf { isClosing(layoutState) } }
if (isClosing) {
DisposableEffect(Unit) {
@@ -188,6 +189,14 @@ fun SceneScope.QuickSettings(
}
}
+private fun isClosing(layoutState: SceneTransitionLayoutState): Boolean {
+ val transitionState = layoutState.transitionState
+ return transitionState is TransitionState.Transition &&
+ !(layoutState.isTransitioning(to = Scenes.Shade) ||
+ layoutState.isTransitioning(to = Scenes.QuickSettings)) &&
+ transitionState.progress >= 0.9f // almost done closing
+}
+
@Composable
private fun QuickSettingsContent(
qsSceneAdapter: QSSceneAdapter,
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 3ec057becc18..491221f39191 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
@@ -361,6 +361,7 @@ private fun SceneScope.SingleShade(
carouselController = mediaCarouselController,
modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.Media),
usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
+ isInSplitShade = false,
)
NotificationScrollingStack(
@@ -565,6 +566,7 @@ private fun SceneScope.SplitShade(
Modifier.zIndex(1f)
},
carouselController = mediaCarouselController,
+ isInSplitShade = true,
)
}
FooterActionsWithAnimatedVisibility(
@@ -619,6 +621,7 @@ private fun SceneScope.ShadeMediaCarousel(
mediaOffsetProvider: ShadeMediaOffsetProvider,
modifier: Modifier = Modifier,
usingCollapsedLandscapeMedia: Boolean = false,
+ isInSplitShade: Boolean,
) {
MediaCarousel(
modifier = modifier.fillMaxWidth(),
@@ -632,5 +635,6 @@ private fun SceneScope.ShadeMediaCarousel(
{ mediaOffsetProvider.offset }
},
usingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia,
+ isInSplitShade = isInSplitShade,
)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 9d3f25e9af22..3bd59df16f12 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -21,6 +21,7 @@ import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import com.android.compose.animation.scene.ContentKey
@@ -249,18 +250,29 @@ sealed interface TransitionState {
private var fromOverscrollSpec: OverscrollSpecImpl? = null
private var toOverscrollSpec: OverscrollSpecImpl? = null
- /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */
- internal val currentOverscrollSpec: OverscrollSpecImpl?
- get() {
- if (this !is HasOverscrollProperties) return null
- val progress = progress
- val bouncingContent = bouncingContent
- return when {
- progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec
- progress > 1f || bouncingContent == toContent -> toOverscrollSpec
- else -> null
+ /**
+ * The current [OverscrollSpecImpl], if this transition is currently overscrolling.
+ *
+ * Note: This is backed by a State<OverscrollSpecImpl?> because the overscroll spec is
+ * derived from progress, and we don't want readers of currentOverscrollSpec to recompose
+ * every time progress is changed.
+ */
+ private val _currentOverscrollSpec: State<OverscrollSpecImpl?>? =
+ if (this !is HasOverscrollProperties) {
+ null
+ } else {
+ derivedStateOf {
+ val progress = progress
+ val bouncingContent = bouncingContent
+ when {
+ progress < 0f || bouncingContent == fromContent -> fromOverscrollSpec
+ progress > 1f || bouncingContent == toContent -> toOverscrollSpec
+ else -> null
+ }
}
}
+ internal val currentOverscrollSpec: OverscrollSpecImpl?
+ get() = _currentOverscrollSpec?.value
/**
* An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index a2c2729e09be..39d46990dc4b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -46,6 +46,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.approachLayout
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
@@ -70,11 +71,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
+import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.assertSizeIsEqualTo
import com.android.compose.test.setContentAndCreateMainScope
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.Assert.assertThrows
@@ -2581,4 +2584,61 @@ class ElementTest {
}
}
}
+
+ @Test
+ fun staticSharedElementShouldNotRemeasureOrReplaceDuringOverscrollableTransition() {
+ val size = 30.dp
+ var numberOfMeasurements = 0
+ var numberOfPlacements = 0
+
+ // Foo is a simple element that does not move or resize during the transition.
+ @Composable
+ fun SceneScope.Foo(modifier: Modifier = Modifier) {
+ Box(
+ modifier
+ .element(TestElements.Foo)
+ .layout { measurable, constraints ->
+ numberOfMeasurements++
+ measurable.measure(constraints).run {
+ numberOfPlacements++
+ layout(width, height) { place(0, 0) }
+ }
+ }
+ .size(size)
+ )
+ }
+
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ val scope =
+ rule.setContentAndCreateMainScope {
+ SceneTransitionLayout(state) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) { Foo() } }
+ }
+ }
+
+ // Start an overscrollable transition driven by progress.
+ var progress by mutableFloatStateOf(0f)
+ val transition = transition(from = SceneA, to = SceneB, progress = { progress })
+ assertThat(transition).isInstanceOf(TransitionState.HasOverscrollProperties::class.java)
+ scope.launch { state.startTransition(transition) }
+
+ // Reset the counters after the first animation frame.
+ rule.waitForIdle()
+ numberOfMeasurements = 0
+ numberOfPlacements = 0
+
+ // Change the progress a bunch of times.
+ val nFrames = 20
+ repeat(nFrames) { i ->
+ progress = i / nFrames.toFloat()
+ rule.waitForIdle()
+
+ // We shouldn't have remeasured or replaced Foo.
+ assertWithMessage("Frame $i didn't remeasure Foo")
+ .that(numberOfMeasurements)
+ .isEqualTo(0)
+ assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0)
+ }
+ }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
new file mode 100644
index 000000000000..9b94c91a348c
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.DigitalClockFaceView
+import com.android.systemui.shared.clocks.view.FlexClockView
+import java.util.Locale
+import java.util.TimeZone
+
+class ComposedDigitalLayerController(
+ private val ctx: Context,
+ private val assets: AssetLoader,
+ private val layer: ComposedDigitalHandLayer,
+ private val isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+) : SimpleClockLayerController {
+ private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
+
+ val layerControllers = mutableListOf<SimpleClockLayerController>()
+ val dozeState = DefaultClockController.AnimationState(1F)
+ var isRegionDark = true
+
+ override var view: DigitalClockFaceView =
+ when (layer.customizedView) {
+ "FlexClockView" -> FlexClockView(ctx, assets, messageBuffer)
+ else -> {
+ throw IllegalStateException("CustomizedView string is not valid")
+ }
+ }
+
+ // Matches LayerControllerConstructor
+ internal constructor(
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+ ) : this(ctx, assets, layer as ComposedDigitalHandLayer, isLargeClock, messageBuffer)
+
+ init {
+ layer.digitalLayers.forEach {
+ val controller =
+ SimpleClockLayerController.Factory.create(
+ ctx,
+ assets,
+ it,
+ isLargeClock,
+ messageBuffer,
+ )
+ view.addView(controller.view)
+ layerControllers.add(controller)
+ }
+ }
+
+ private fun refreshTime() {
+ layerControllers.forEach { it.faceEvents.onTimeTick() }
+ view.refreshTime()
+ }
+
+ override val events =
+ object : ClockEvents {
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ layerControllers.forEach { it.events.onTimeZoneChanged(timeZone) }
+ refreshTime()
+ }
+
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ layerControllers.forEach { it.events.onTimeFormatChanged(is24Hr) }
+ refreshTime()
+ }
+
+ override fun onLocaleChanged(locale: Locale) {
+ layerControllers.forEach { it.events.onLocaleChanged(locale) }
+ view.onLocaleChanged(locale)
+ refreshTime()
+ }
+
+ override fun onWeatherDataChanged(data: WeatherData) {
+ view.onWeatherDataChanged(data)
+ }
+
+ override fun onAlarmDataChanged(data: AlarmData) {
+ view.onAlarmDataChanged(data)
+ }
+
+ override fun onZenDataChanged(data: ZenData) {
+ view.onZenDataChanged(data)
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {}
+
+ override fun onSeedColorChanged(seedColor: Int?) {}
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+
+ override var isReactiveTouchInteractionEnabled
+ get() = view.isReactiveTouchInteractionEnabled
+ set(value) {
+ view.isReactiveTouchInteractionEnabled = value
+ }
+ }
+
+ override fun updateColors() {
+ view.updateColors(assets, isRegionDark)
+ }
+
+ override val animations =
+ object : ClockAnimations {
+ override fun enter() {
+ refreshTime()
+ }
+
+ override fun doze(fraction: Float) {
+ val (hasChanged, hasJumped) = dozeState.update(fraction)
+ if (hasChanged) view.animateDoze(dozeState.isActive, !hasJumped)
+ view.dozeFraction = fraction
+ view.invalidate()
+ }
+
+ override fun fold(fraction: Float) {
+ refreshTime()
+ }
+
+ override fun charge() {
+ view.animateCharge()
+ }
+
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
+ view.onPositionUpdated(fromLeft, direction, fraction)
+ }
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {}
+
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {
+ view.onPickerCarouselSwiping(swipingFraction)
+ }
+ }
+
+ override val faceEvents =
+ object : ClockFaceEvents {
+ override fun onTimeTick() {
+ refreshTime()
+ }
+
+ override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+ this@ComposedDigitalLayerController.isRegionDark = isRegionDark
+ updateColors()
+ }
+
+ override fun onFontSettingChanged(fontSizePx: Float) {
+ view.onFontSettingChanged(fontSizePx)
+ }
+
+ override fun onTargetRegionChanged(targetRegion: Rect?) {}
+
+ override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+ }
+
+ override val config =
+ ClockFaceConfig(
+ hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay,
+ hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation,
+ useCustomClockScene = view.useCustomClockScene,
+ )
+
+ @VisibleForTesting
+ override var fakeTimeMills: Long? = null
+ get() = field
+ set(timeInMills) {
+ field = timeInMills
+ for (layerController in layerControllers) {
+ layerController.fakeTimeMills = timeInMills
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 07191c671a34..ac268420fb75 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -24,6 +24,8 @@ import com.android.systemui.plugins.clocks.ClockMetadata
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_ID = "DEFAULT"
@@ -33,8 +35,9 @@ class DefaultClockProvider(
val ctx: Context,
val layoutInflater: LayoutInflater,
val resources: Resources,
- val hasStepClockAnimation: Boolean = false,
- val migratedClocks: Boolean = false,
+ private val hasStepClockAnimation: Boolean = false,
+ private val migratedClocks: Boolean = false,
+ private val clockReactiveVariants: Boolean = false,
) : ClockProvider {
private var messageBuffers: ClockMessageBuffers? = null
@@ -49,15 +52,23 @@ class DefaultClockProvider(
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- return DefaultClockController(
- ctx,
- layoutInflater,
- resources,
- settings,
- hasStepClockAnimation,
- migratedClocks,
- messageBuffers,
- )
+ return if (clockReactiveVariants) {
+ // TODO handle the case here where only the smallClock message buffer is added
+ val assetLoader =
+ AssetLoader(ctx, ctx, "clocks/", messageBuffers?.smallClockMessageBuffer!!)
+
+ SimpleClockController(ctx, assetLoader, FLEX_DESIGN, messageBuffers)
+ } else {
+ DefaultClockController(
+ ctx,
+ layoutInflater,
+ resources,
+ settings,
+ hasStepClockAnimation,
+ migratedClocks,
+ messageBuffers,
+ )
+ }
}
override fun getClockPickerConfig(id: ClockId): ClockPickerConfig {
@@ -73,4 +84,163 @@ class DefaultClockProvider(
resources.getDrawable(R.drawable.clock_default_thumbnail, null),
)
}
+
+ companion object {
+ val FLEX_DESIGN = run {
+ val largeLayer =
+ listOf(
+ ComposedDigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ customizedView = "FlexClockView",
+ digitalLayers =
+ listOf(
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.FIRST_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "hh"
+ ),
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.SECOND_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "hh"
+ ),
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.FIRST_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "mm"
+ ),
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.SECOND_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "mm"
+ )
+ )
+ )
+ )
+
+ val smallLayer =
+ listOf(
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ fontSizeScale = 0.98f,
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ ),
+ alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+ dateTimeFormat = "h:mm"
+ )
+ )
+
+ ClockDesign(
+ id = DEFAULT_CLOCK_ID,
+ name = "@string/clock_default_name",
+ description = "@string/clock_default_description",
+ large = ClockFace(layers = largeLayer),
+ small = ClockFace(layers = smallLayer)
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt
new file mode 100644
index 000000000000..ef8bee0875d2
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.graphics.Rect
+import android.view.View
+
+fun computeLayoutDiff(
+ view: View,
+ targetRegion: Rect,
+ isLargeClock: Boolean,
+): Pair<Float, Float> {
+ val parent = view.parent
+ if (parent is View && parent.isLaidOut() && isLargeClock) {
+ return Pair(
+ targetRegion.centerX() - parent.width / 2f,
+ targetRegion.centerY() - parent.height / 2f
+ )
+ }
+ return Pair(0f, 0f)
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt
new file mode 100644
index 000000000000..ec7779825bda
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockConfig
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+
+/** Controller for a simple json specified clock */
+class SimpleClockController(
+ private val ctx: Context,
+ private val assets: AssetLoader,
+ val design: ClockDesign,
+ val messageBuffers: ClockMessageBuffers?,
+) : ClockController {
+ override val smallClock = run {
+ val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+ SimpleClockFaceController(
+ ctx,
+ assets.copy(messageBuffer = buffer),
+ design.small ?: design.large!!,
+ false,
+ buffer,
+ )
+ }
+
+ override val largeClock = run {
+ val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+ SimpleClockFaceController(
+ ctx,
+ assets.copy(messageBuffer = buffer),
+ design.large ?: design.small!!,
+ true,
+ buffer,
+ )
+ }
+
+ override val config: ClockConfig by lazy {
+ ClockConfig(
+ design.id,
+ design.name?.let { assets.tryReadString(it) ?: it } ?: "",
+ design.description?.let { assets.tryReadString(it) ?: it } ?: "",
+ isReactiveToTone =
+ design.colorPalette == null || design.colorPalette == MonetStyle.CLOCK,
+ useAlternateSmartspaceAODTransition =
+ smallClock.config.hasCustomWeatherDataDisplay ||
+ largeClock.config.hasCustomWeatherDataDisplay,
+ useCustomClockScene =
+ smallClock.config.useCustomClockScene || largeClock.config.useCustomClockScene,
+ )
+ }
+
+ override val events =
+ object : ClockEvents {
+ override var isReactiveTouchInteractionEnabled = false
+ set(value) {
+ field = value
+ smallClock.events.isReactiveTouchInteractionEnabled = value
+ largeClock.events.isReactiveTouchInteractionEnabled = value
+ }
+
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ smallClock.events.onTimeZoneChanged(timeZone)
+ largeClock.events.onTimeZoneChanged(timeZone)
+ }
+
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ smallClock.events.onTimeFormatChanged(is24Hr)
+ largeClock.events.onTimeFormatChanged(is24Hr)
+ }
+
+ override fun onLocaleChanged(locale: Locale) {
+ smallClock.events.onLocaleChanged(locale)
+ largeClock.events.onLocaleChanged(locale)
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {
+ assets.refreshColorPalette(design.colorPalette)
+ smallClock.assets.refreshColorPalette(design.colorPalette)
+ largeClock.assets.refreshColorPalette(design.colorPalette)
+
+ smallClock.events.onColorPaletteChanged(resources)
+ largeClock.events.onColorPaletteChanged(resources)
+ }
+
+ override fun onSeedColorChanged(seedColor: Int?) {
+ assets.setSeedColor(seedColor, design.colorPalette)
+ smallClock.assets.setSeedColor(seedColor, design.colorPalette)
+ largeClock.assets.setSeedColor(seedColor, design.colorPalette)
+
+ smallClock.events.onSeedColorChanged(seedColor)
+ largeClock.events.onSeedColorChanged(seedColor)
+ }
+
+ override fun onWeatherDataChanged(data: WeatherData) {
+ smallClock.events.onWeatherDataChanged(data)
+ largeClock.events.onWeatherDataChanged(data)
+ }
+
+ override fun onAlarmDataChanged(data: AlarmData) {
+ smallClock.events.onAlarmDataChanged(data)
+ largeClock.events.onAlarmDataChanged(data)
+ }
+
+ override fun onZenDataChanged(data: ZenData) {
+ smallClock.events.onZenDataChanged(data)
+ largeClock.events.onZenDataChanged(data)
+ }
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {
+ smallClock.events.onReactiveAxesChanged(axes)
+ largeClock.events.onReactiveAxesChanged(axes)
+ }
+ }
+
+ override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+ events.onColorPaletteChanged(resources)
+ smallClock.animations.doze(dozeFraction)
+ largeClock.animations.doze(dozeFraction)
+ smallClock.animations.fold(foldFraction)
+ largeClock.animations.fold(foldFraction)
+ smallClock.events.onTimeTick()
+ largeClock.events.onTimeTick()
+ }
+
+ override fun dump(pw: PrintWriter) {}
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt
new file mode 100644
index 000000000000..ef398d1a52a0
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceController
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockFaceLayout
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.DigitalClockFaceView
+import java.util.Locale
+import java.util.TimeZone
+import kotlin.math.max
+
+interface ClockEventUnion : ClockEvents, ClockFaceEvents
+
+class SimpleClockFaceController(
+ ctx: Context,
+ val assets: AssetLoader,
+ face: ClockFace,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+) : ClockFaceController {
+ override val view: View
+ override val config: ClockFaceConfig by lazy {
+ ClockFaceConfig(
+ hasCustomWeatherDataDisplay = layers.any { it.config.hasCustomWeatherDataDisplay },
+ hasCustomPositionUpdatedAnimation =
+ layers.any { it.config.hasCustomPositionUpdatedAnimation },
+ tickRate = getTickRate(),
+ useCustomClockScene = layers.any { it.config.useCustomClockScene },
+ )
+ }
+
+ val layers = mutableListOf<SimpleClockLayerController>()
+
+ val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
+
+ init {
+ val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ lp.gravity = Gravity.CENTER
+ view =
+ if (face.layers.size == 1) {
+ // Optimize a clocks with a single layer by excluding the face level view group. We
+ // expect the view container from the host process to always be a FrameLayout.
+ val layer = face.layers[0]
+ val controller =
+ SimpleClockLayerController.Factory.create(
+ ctx,
+ assets,
+ layer,
+ isLargeClock,
+ messageBuffer,
+ )
+ layers.add(controller)
+ controller.view.layoutParams = lp
+ controller.view
+ } else {
+ // For multiple views, we use an intermediate RelativeLayout so that we can do some
+ // intelligent laying out between the children views.
+ val group = SimpleClockRelativeLayout(ctx, face.faceLayout)
+ group.layoutParams = lp
+ group.gravity = Gravity.CENTER
+ group.clipChildren = false
+ for (layer in face.layers) {
+ face.faceLayout?.let {
+ if (layer is DigitalHandLayer) {
+ layer.faceLayout = it
+ }
+ }
+ val controller =
+ SimpleClockLayerController.Factory.create(
+ ctx,
+ assets,
+ layer,
+ isLargeClock,
+ messageBuffer,
+ )
+ group.addView(controller.view)
+ layers.add(controller)
+ }
+ group
+ }
+ }
+
+ override val layout: ClockFaceLayout =
+ DefaultClockFaceLayout(view).apply {
+ views[0].id =
+ if (isLargeClock) {
+ assets.getResourcesId("lockscreen_clock_view_large")
+ } else {
+ assets.getResourcesId("lockscreen_clock_view")
+ }
+ }
+
+ override val events =
+ object : ClockEventUnion {
+ override var isReactiveTouchInteractionEnabled = false
+ get() = field
+ set(value) {
+ field = value
+ layers.forEach { it.events.isReactiveTouchInteractionEnabled = value }
+ }
+
+ override fun onTimeTick() {
+ timespecHandler.updateTime()
+ if (
+ config.tickRate == ClockTickRate.PER_MINUTE ||
+ view.contentDescription != timespecHandler.getContentDescription()
+ ) {
+ view.contentDescription = timespecHandler.getContentDescription()
+ }
+ layers.forEach { it.faceEvents.onTimeTick() }
+ }
+
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ timespecHandler.timeZone = timeZone
+ layers.forEach { it.events.onTimeZoneChanged(timeZone) }
+ }
+
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ timespecHandler.is24Hr = is24Hr
+ layers.forEach { it.events.onTimeFormatChanged(is24Hr) }
+ }
+
+ override fun onLocaleChanged(locale: Locale) {
+ timespecHandler.updateLocale(locale)
+ layers.forEach { it.events.onLocaleChanged(locale) }
+ }
+
+ override fun onFontSettingChanged(fontSizePx: Float) {
+ layers.forEach { it.faceEvents.onFontSettingChanged(fontSizePx) }
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {
+ layers.forEach {
+ it.events.onColorPaletteChanged(resources)
+ it.updateColors()
+ }
+ }
+
+ override fun onSeedColorChanged(seedColor: Int?) {
+ layers.forEach {
+ it.events.onSeedColorChanged(seedColor)
+ it.updateColors()
+ }
+ }
+
+ override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+ layers.forEach { it.faceEvents.onRegionDarknessChanged(isRegionDark) }
+ }
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+
+ /**
+ * targetRegion passed to all customized clock applies counter translationY of
+ * KeyguardStatusView and keyguard_large_clock_top_margin from default clock
+ */
+ override fun onTargetRegionChanged(targetRegion: Rect?) {
+ // When a clock needs to be aligned with screen, like weather clock
+ // it needs to offset back the translation of keyguard_large_clock_top_margin
+ if (view is DigitalClockFaceView && view.isAlignedWithScreen()) {
+ val topMargin = getKeyguardLargeClockTopMargin(assets)
+ targetRegion?.let {
+ val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock)
+ // In LS, we use yDiff to counter translate
+ // the translation of KeyguardLargeClockTopMargin
+ // With the targetRegion passed from picker,
+ // we will have yDiff = 0, no translation is needed for weather clock
+ if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2
+ }
+ return
+ }
+
+ var maxWidth = 0f
+ var maxHeight = 0f
+
+ for (layer in layers) {
+ layer.faceEvents.onTargetRegionChanged(targetRegion)
+ maxWidth = max(maxWidth, layer.view.layoutParams.width.toFloat())
+ maxHeight = max(maxHeight, layer.view.layoutParams.height.toFloat())
+ }
+
+ val lp =
+ if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) {
+ // No specified width/height. Just match parent size.
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ } else {
+ // Scale to fit in targetRegion based on largest child elements.
+ val ratio = maxWidth / maxHeight
+ val targetRatio = targetRegion.width() / targetRegion.height().toFloat()
+ val scale =
+ if (ratio > targetRatio) targetRegion.width() / maxWidth
+ else targetRegion.height() / maxHeight
+
+ FrameLayout.LayoutParams(
+ (maxWidth * scale).toInt(),
+ (maxHeight * scale).toInt(),
+ )
+ }
+
+ lp.gravity = Gravity.CENTER
+ view.layoutParams = lp
+ targetRegion?.let {
+ val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock)
+ view.translationX = xDiff
+ view.translationY = yDiff
+ }
+ }
+
+ override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+
+ override fun onWeatherDataChanged(data: WeatherData) {
+ layers.forEach { it.events.onWeatherDataChanged(data) }
+ }
+
+ override fun onAlarmDataChanged(data: AlarmData) {
+ layers.forEach { it.events.onAlarmDataChanged(data) }
+ }
+
+ override fun onZenDataChanged(data: ZenData) {
+ layers.forEach { it.events.onZenDataChanged(data) }
+ }
+ }
+
+ override val animations =
+ object : ClockAnimations {
+ override fun enter() {
+ layers.forEach { it.animations.enter() }
+ }
+
+ override fun doze(fraction: Float) {
+ layers.forEach { it.animations.doze(fraction) }
+ }
+
+ override fun fold(fraction: Float) {
+ layers.forEach { it.animations.fold(fraction) }
+ }
+
+ override fun charge() {
+ layers.forEach { it.animations.charge() }
+ }
+
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {
+ face.pickerScale?.let {
+ view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX
+ view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY
+ }
+ if (!(view is DigitalClockFaceView && view.isAlignedWithScreen())) {
+ val topMargin = getKeyguardLargeClockTopMargin(assets)
+ view.translationY = topMargin / 2F * swipingFraction
+ }
+ layers.forEach { it.animations.onPickerCarouselSwiping(swipingFraction) }
+ view.invalidate()
+ }
+
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
+ layers.forEach { it.animations.onPositionUpdated(fromLeft, direction, fraction) }
+ }
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {
+ layers.forEach { it.animations.onPositionUpdated(distance, fraction) }
+ }
+ }
+
+ private fun getTickRate(): ClockTickRate {
+ var tickRate = ClockTickRate.PER_MINUTE
+ for (layer in layers) {
+ if (layer.config.tickRate.value < tickRate.value) {
+ tickRate = layer.config.tickRate
+ }
+ }
+ return tickRate
+ }
+
+ private fun getKeyguardLargeClockTopMargin(assets: AssetLoader): Int {
+ val topMarginRes =
+ assets.resolveResourceId(null, "dimen", "keyguard_large_clock_top_margin")
+ if (topMarginRes != null) {
+ val (res, id) = topMarginRes
+ return res.getDimensionPixelSize(id)
+ }
+ return 0
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
new file mode 100644
index 000000000000..f71543efa650
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import kotlin.reflect.KClass
+
+typealias LayerControllerConstructor =
+ (
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+ ) -> SimpleClockLayerController
+
+interface SimpleClockLayerController {
+ val view: View
+ val events: ClockEvents
+ val animations: ClockAnimations
+ val faceEvents: ClockFaceEvents
+ val config: ClockFaceConfig
+
+ @VisibleForTesting var fakeTimeMills: Long?
+
+ // Called immediately after either onColorPaletteChanged or onSeedColorChanged is called.
+ // Provided for convience to not duplicate color update logic after state updated.
+ fun updateColors() {}
+
+ companion object Factory {
+ val constructorMap = mutableMapOf<Pair<KClass<*>, KClass<*>?>, LayerControllerConstructor>()
+
+ internal inline fun <reified TLayer> registerConstructor(
+ noinline constructor: LayerControllerConstructor,
+ ) where TLayer : ClockLayer {
+ constructorMap[Pair(TLayer::class, null)] = constructor
+ }
+
+ inline fun <reified TLayer, reified TStyle> registerTextConstructor(
+ noinline constructor: LayerControllerConstructor,
+ ) where TLayer : ClockLayer, TStyle : TextStyle {
+ constructorMap[Pair(TLayer::class, TStyle::class)] = constructor
+ }
+
+ init {
+ registerConstructor<ComposedDigitalHandLayer>(::ComposedDigitalLayerController)
+ registerTextConstructor<DigitalHandLayer, FontTextStyle>(::createSimpleDigitalLayer)
+ }
+
+ private fun createSimpleDigitalLayer(
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer
+ ): SimpleClockLayerController {
+ val view = SimpleDigitalClockTextView(ctx, messageBuffer)
+ return SimpleDigitalHandLayerController(
+ ctx,
+ assets,
+ layer as DigitalHandLayer,
+ view,
+ messageBuffer
+ )
+ }
+
+ fun create(
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer
+ ): SimpleClockLayerController {
+ val styleClass = if (layer is DigitalHandLayer) layer.style::class else null
+ val key = Pair(layer::class, styleClass)
+ return constructorMap[key]?.invoke(ctx, assets, layer, isLargeClock, messageBuffer)
+ ?: throw IllegalArgumentException("Unrecognized ClockLayer type: $key")
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
new file mode 100644
index 000000000000..6e1b9aabf86d
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.view.View.MeasureSpec.EXACTLY
+import android.widget.RelativeLayout
+import androidx.core.view.children
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+
+class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) :
+ RelativeLayout(context) {
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // For migrate_clocks_to_blueprint, mode is EXACTLY
+ // when the flag is turned off, we won't execute this codes
+ if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+ if (
+ faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL ||
+ faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER
+ ) {
+ val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F
+ children.forEach {
+ // The assumption here is the height of text view is linear to font size
+ (it as SimpleDigitalClockView).applyTextSize(
+ constrainedHeight,
+ constrainedByHeight = true,
+ )
+ }
+ }
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
new file mode 100644
index 000000000000..a3240f81e499
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+import java.util.Locale
+import java.util.TimeZone
+
+private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
+
+open class SimpleDigitalHandLayerController<T>(
+ private val ctx: Context,
+ private val assets: AssetLoader,
+ private val layer: DigitalHandLayer,
+ override val view: T,
+ messageBuffer: MessageBuffer,
+) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
+ private val logger = Logger(messageBuffer, TAG)
+ val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
+
+ @VisibleForTesting
+ fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
+
+ @VisibleForTesting
+ override var fakeTimeMills: Long?
+ get() = timespec.fakeTimeMills
+ set(value) {
+ timespec.fakeTimeMills = value
+ }
+
+ override val config = ClockFaceConfig()
+ var dozeState: DefaultClockController.AnimationState? = null
+ var isRegionDark: Boolean = true
+
+ init {
+ view.layoutParams =
+ RelativeLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ if (layer.alignment != null) {
+ layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
+ layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
+ }
+ view.applyStyles(assets, layer.style, layer.aodStyle)
+ view.id =
+ ctx.resources.getIdentifier(
+ generateDigitalLayerIdString(layer),
+ "id",
+ ctx.getPackageName(),
+ )
+ }
+
+ fun applyLayout(layout: DigitalFaceLayout?) {
+ when (layout) {
+ DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER,
+ DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout)
+ DigitalFaceLayout.TWO_PAIRS_HORIZONTAL,
+ DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout)
+ else -> {
+ // one view always use FrameLayout
+ // no need to change here
+ }
+ }
+ applyMargin()
+ }
+
+ private fun applyMargin() {
+ if (view.layoutParams is RelativeLayout.LayoutParams) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ layer.marginRatio?.let {
+ lp.setMargins(
+ /* left = */ (it.left * view.measuredWidth).toInt(),
+ /* top = */ (it.top * view.measuredHeight).toInt(),
+ /* right = */ (it.right * view.measuredWidth).toInt(),
+ /* bottom = */ (it.bottom * view.measuredHeight).toInt(),
+ )
+ }
+ view.layoutParams = lp
+ }
+ }
+
+ private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
+ if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) {
+ when (view.id) {
+ R.id.HOUR_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ }
+ R.id.MINUTE_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
+ }
+ else -> {
+ throw Exception("cannot apply two pairs layout to view ${view.id}")
+ }
+ }
+ } else {
+ when (view.id) {
+ R.id.HOUR_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
+ }
+ R.id.MINUTE_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
+ lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR)
+ }
+ else -> {
+ throw Exception("cannot apply two pairs layout to view ${view.id}")
+ }
+ }
+ }
+ view.layoutParams = lp
+ }
+
+ private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ when (fourDigitsfaceLayout) {
+ DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> {
+ when (view.id) {
+ R.id.HOUR_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
+ }
+ R.id.HOUR_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
+ lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT)
+ }
+ R.id.MINUTE_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT)
+ lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT)
+ }
+ R.id.MINUTE_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT)
+ lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT)
+ }
+ else -> {
+ throw Exception("cannot apply four digits layout to view ${view.id}")
+ }
+ }
+ }
+ DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> {
+ when (view.id) {
+ R.id.HOUR_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ }
+ R.id.HOUR_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
+ }
+ R.id.MINUTE_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT)
+ }
+ R.id.MINUTE_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT)
+ }
+ else -> {
+ throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}")
+ }
+ }
+ }
+ else -> {
+ throw IllegalArgumentException(
+ "applyFourDigitsLayout function should not " +
+ "have parameters as ${layer.faceLayout}"
+ )
+ }
+ }
+ if (lp == view.layoutParams) {
+ return
+ }
+ view.layoutParams = lp
+ }
+
+ fun refreshTime() {
+ timespec.updateTime()
+ val text = timespec.getDigitString()
+ if (view.text != text) {
+ view.text = text
+ view.refreshTime()
+ logger.d({ "refreshTime: new text=$str1" }) { str1 = text }
+ }
+ }
+
+ override val events =
+ object : ClockEvents {
+ override var isReactiveTouchInteractionEnabled = false
+
+ override fun onLocaleChanged(locale: Locale) {
+ timespec.updateLocale(locale)
+ refreshTime()
+ }
+
+ /** Call whenever the text time format changes (12hr vs 24hr) */
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ timespec.is24Hr = is24Hr
+ refreshTime()
+ }
+
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ timespec.timeZone = timeZone
+ refreshTime()
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {}
+
+ override fun onSeedColorChanged(seedColor: Int?) {}
+
+ override fun onWeatherDataChanged(data: WeatherData) {}
+
+ override fun onAlarmDataChanged(data: AlarmData) {}
+
+ override fun onZenDataChanged(data: ZenData) {}
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+ }
+
+ override fun updateColors() {
+ view.updateColors(assets, isRegionDark)
+ refreshTime()
+ }
+
+ override val animations =
+ object : ClockAnimations {
+ override fun enter() {
+ applyLayout(layer.faceLayout)
+ refreshTime()
+ }
+
+ override fun doze(fraction: Float) {
+ if (dozeState == null) {
+ dozeState = DefaultClockController.AnimationState(fraction)
+ view.animateDoze(dozeState!!.isActive, false)
+ } else {
+ val (hasChanged, hasJumped) = dozeState!!.update(fraction)
+ if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped)
+ }
+ view.dozeFraction = fraction
+ }
+
+ override fun fold(fraction: Float) {
+ applyLayout(layer.faceLayout)
+ refreshTime()
+ }
+
+ override fun charge() {
+ view.animateCharge()
+ }
+
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {}
+
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {}
+ }
+
+ override val faceEvents =
+ object : ClockFaceEvents {
+ override fun onTimeTick() {
+ refreshTime()
+ if (
+ layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
+ layer.timespec == DigitalTimespec.DATE_FORMAT
+ ) {
+ view.contentDescription = timespec.getContentDescription()
+ }
+ }
+
+ override fun onFontSettingChanged(fontSizePx: Float) {
+ view.applyTextSize(fontSizePx)
+ applyMargin()
+ }
+
+ override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+ this@SimpleDigitalHandLayerController.isRegionDark = isRegionDark
+ updateColors()
+ }
+
+ override fun onTargetRegionChanged(targetRegion: Rect?) {}
+
+ override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+ }
+
+ companion object {
+ private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0"
+ private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0"
+
+ fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) =
+ assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR)
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
new file mode 100644
index 000000000000..ed6a403a7c7e
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.icu.text.DateFormat
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone as IcuTimeZone
+import android.icu.util.ULocale
+import androidx.annotation.VisibleForTesting
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+open class TimespecHandler(
+ val cal: Calendar,
+) {
+ var timeZone: TimeZone
+ get() = cal.timeZone
+ set(value) {
+ cal.timeZone = value
+ onTimeZoneChanged()
+ }
+
+ @VisibleForTesting var fakeTimeMills: Long? = null
+
+ fun updateTime() {
+ var timeMs = fakeTimeMills ?: System.currentTimeMillis()
+ cal.timeInMillis = (timeMs * TIME_TRAVEL_SCALE).toLong()
+ }
+
+ protected open fun onTimeZoneChanged() {}
+
+ companion object {
+ // Modifying this will cause the clock to run faster or slower. This is a useful way of
+ // manually checking that clocks are correctly animating through time.
+ private const val TIME_TRAVEL_SCALE = 1.0
+ }
+}
+
+class DigitalTimespecHandler(
+ val timespec: DigitalTimespec,
+ private val timeFormat: String,
+ cal: Calendar = Calendar.getInstance(),
+) : TimespecHandler(cal) {
+ var is24Hr = false
+ set(value) {
+ field = value
+ applyPattern()
+ }
+
+ private var dateFormat = updateSimpleDateFormat(Locale.getDefault())
+ private var contentDescriptionFormat = getContentDescriptionFormat(Locale.getDefault())
+
+ init {
+ applyPattern()
+ }
+
+ override fun onTimeZoneChanged() {
+ dateFormat.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
+ contentDescriptionFormat?.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
+ applyPattern()
+ }
+
+ fun updateLocale(locale: Locale) {
+ dateFormat = updateSimpleDateFormat(locale)
+ contentDescriptionFormat = getContentDescriptionFormat(locale)
+ onTimeZoneChanged()
+ }
+
+ private fun updateSimpleDateFormat(locale: Locale): DateFormat {
+ if (
+ locale.language.equals(Locale.ENGLISH.language) ||
+ timespec != DigitalTimespec.DATE_FORMAT
+ ) {
+ // force date format in English, and time format to use format defined in json
+ return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
+ } else {
+ return SimpleDateFormat.getInstanceForSkeleton(timeFormat, locale)
+ }
+ }
+
+ private fun getContentDescriptionFormat(locale: Locale): DateFormat? {
+ return when (timespec) {
+ DigitalTimespec.TIME_FULL_FORMAT ->
+ SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
+ DigitalTimespec.DATE_FORMAT ->
+ SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
+ else -> {
+ null
+ }
+ }
+ }
+
+ private fun applyPattern() {
+ val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH")
+ val format = if (is24Hr) timeFormat24Hour else timeFormat
+ if (timespec != DigitalTimespec.DATE_FORMAT) {
+ (dateFormat as SimpleDateFormat).applyPattern(format)
+ (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
+ if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
+ else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
+ )
+ }
+ }
+
+ private fun getSingleDigit(): String {
+ val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT
+ val text = dateFormat.format(cal.time).toString()
+ return text.substring(
+ if (isFirstDigit) 0 else text.length - 1,
+ if (isFirstDigit) text.length - 1 else text.length
+ )
+ }
+
+ fun getDigitString(): String {
+ return when (timespec) {
+ DigitalTimespec.FIRST_DIGIT,
+ DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
+ DigitalTimespec.DIGIT_PAIR -> {
+ dateFormat.format(cal.time).toString()
+ }
+ DigitalTimespec.TIME_FULL_FORMAT -> {
+ dateFormat.format(cal.time).toString()
+ }
+ DigitalTimespec.DATE_FORMAT -> {
+ dateFormat.format(cal.time).toString().uppercase()
+ }
+ }
+ }
+
+ fun getContentDescription(): String? {
+ return when (timespec) {
+ DigitalTimespec.TIME_FULL_FORMAT,
+ DigitalTimespec.DATE_FORMAT -> {
+ contentDescriptionFormat?.format(cal.time).toString()
+ }
+ else -> {
+ return null
+ }
+ }
+ }
+
+ companion object {
+ const val CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR = "hh:mm"
+ const val CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR = "HH:mm"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index d63e728cf443..d63e728cf443 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 2c1dacdfae73..2c1dacdfae73 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 5fe5cb3fe43c..5fe5cb3fe43c 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
index 7bb6ef1c8895..7bb6ef1c8895 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index 9cd52153eff6..9cd52153eff6 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index 3c229975eef5..3c229975eef5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
index 8b5372a1f035..8b5372a1f035 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
index a676c7db4290..09831059a4b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
@@ -31,12 +31,11 @@ import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.core.FakeLogBuffer
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -78,7 +77,7 @@ class ScreenBrightnessDisplayManagerRepositoryTest : SysuiTestCase() {
displayId,
displayManager,
FakeLogBuffer.Factory.create(),
- mock<TableLogBuffer>(),
+ logcatTableLogBuffer(kosmos, "screenBrightness"),
kosmos.applicationCoroutineScope,
kosmos.testDispatcher,
)
@@ -163,7 +162,7 @@ class ScreenBrightnessDisplayManagerRepositoryTest : SysuiTestCase() {
changeBrightnessInfoAndNotify(
BrightnessInfo(0.5f, 0.1f, 0.7f),
- listenerCaptor.value
+ listenerCaptor.value,
)
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
index b6616bf0c8de..18e7a7e28b4d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
@@ -27,9 +27,8 @@ import com.android.systemui.brightness.shared.model.LinearBrightness
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
@@ -49,7 +48,7 @@ class ScreenBrightnessInteractorTest : SysuiTestCase() {
ScreenBrightnessInteractor(
screenBrightnessRepository,
applicationCoroutineScope,
- mock<TableLogBuffer>()
+ logcatTableLogBuffer(this, "screenBrightness"),
)
}
@@ -112,7 +111,7 @@ class ScreenBrightnessInteractorTest : SysuiTestCase() {
BrightnessUtils.convertGammaToLinearFloat(
gammaBrightness,
min.floatValue,
- max.floatValue
+ max.floatValue,
)
assertThat(temporaryBrightness!!.floatValue)
.isWithin(1e-5f)
@@ -136,7 +135,7 @@ class ScreenBrightnessInteractorTest : SysuiTestCase() {
BrightnessUtils.convertGammaToLinearFloat(
gammaBrightness,
min.floatValue,
- max.floatValue
+ max.floatValue,
)
assertThat(brightness!!.floatValue).isWithin(1e-5f).of(expectedBrightness)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
index 86788d35a670..86788d35a670 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
index dd5ad17fd875..2b0928ffd396 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt
@@ -21,7 +21,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.testKosmos
@@ -44,7 +44,6 @@ import org.mockito.kotlin.mock
class CommunalMediaRepositoryImplTest : SysuiTestCase() {
private val mediaDataManager = mock<MediaDataManager>()
private val mediaData = mock<MediaData>()
- private val tableLogBuffer = mock<TableLogBuffer>()
private lateinit var underTest: CommunalMediaRepositoryImpl
@@ -52,14 +51,11 @@ class CommunalMediaRepositoryImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val tableLogBuffer = logcatTableLogBuffer(kosmos, "CommunalMediaRepositoryImplTest")
@Before
fun setUp() {
- underTest =
- CommunalMediaRepositoryImpl(
- mediaDataManager,
- tableLogBuffer,
- )
+ underTest = CommunalMediaRepositoryImpl(mediaDataManager, tableLogBuffer)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 179ba2256442..cecc11e5ffd4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -19,7 +19,6 @@ package com.android.systemui.communal.view.viewmodel
import android.appwidget.AppWidgetProviderInfo
import android.content.ActivityNotFoundException
import android.content.ComponentName
-import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.UserInfo
import android.provider.Settings
@@ -27,7 +26,6 @@ import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.accessibilityManager
import android.widget.RemoteViews
-import androidx.activity.result.ActivityResultLauncher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
@@ -88,7 +86,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var packageManager: PackageManager
- @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
@Mock private lateinit var metricsLogger: CommunalMetricsLogger
private val kosmos = testKosmos()
@@ -117,10 +114,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
communalSceneInteractor = kosmos.communalSceneInteractor
communalInteractor = spy(kosmos.communalInteractor)
kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
- kosmos.fakeUserTracker.set(
- userInfos = listOf(MAIN_USER_INFO),
- selectedUserIndex = 0,
- )
+ kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
accessibilityManager = kosmos.accessibilityManager
@@ -257,10 +251,13 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
@Test
fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
testScope.runTest {
+ var activityStarted = false
val success =
- underTest.onOpenWidgetPicker(testableResources.resources, activityResultLauncher)
+ underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
+ run { activityStarted = true }
+ }
- verify(activityResultLauncher).launch(any())
+ assertTrue(activityStarted)
assertTrue(success)
}
}
@@ -268,14 +265,10 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
@Test
fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
testScope.runTest {
- whenever(activityResultLauncher.launch(any()))
- .thenThrow(ActivityNotFoundException::class.java)
-
val success =
- underTest.onOpenWidgetPicker(
- testableResources.resources,
- activityResultLauncher,
- )
+ underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
+ run { throw ActivityNotFoundException() }
+ }
assertFalse(success)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
index d9dcfdc7becb..9c6fd4bf71b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -56,6 +56,7 @@ class DreamOverlayCallbackControllerTest : SysuiTestCase() {
// Adding twice should not invoke twice
reset(callback)
+ underTest.onStartDream()
underTest.addCallback(callback)
underTest.onWakeUp()
verify(callback, times(1)).onWakeUp()
@@ -68,6 +69,19 @@ class DreamOverlayCallbackControllerTest : SysuiTestCase() {
}
@Test
+ fun onWakeUp_multipleCalls() {
+ underTest.onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ underTest.onWakeUp()
+ underTest.onWakeUp()
+ verify(callback, times(1)).onWakeUp()
+ assertThat(underTest.isDreaming).isEqualTo(false)
+ }
+
+ @Test
fun onStartDreamInvokesCallback() {
underTest.addCallback(callback)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index a3314e8900ce..f5d2d42902d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -712,6 +712,9 @@ class DreamOverlayServiceTest : SysuiTestCase() {
// Verify DreamOverlayContainerViewController is destroyed.
verify(mDreamOverlayContainerViewController).destroy()
+
+ // DreamOverlay callback receives onWakeUp.
+ verify(mDreamOverlayCallbackController).onWakeUp()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
index 13f30f560cdf..945e44afa455 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -46,6 +46,7 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.isNull
@ExperimentalCoroutinesApi
@SmallTest
@@ -96,7 +97,7 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
.sendVolumeKeyEvent(
eq(actionDownVolumeDownKeyEvent),
eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
- eq(true)
+ eq(true),
)
assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isTrue()
@@ -104,7 +105,7 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
.sendVolumeKeyEvent(
eq(actionDownVolumeUpKeyEvent),
eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
- eq(true)
+ eq(true),
)
}
@@ -117,7 +118,7 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
.sendVolumeKeyEvent(
eq(actionDownVolumeDownKeyEvent),
eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
- eq(true)
+ eq(true),
)
assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isFalse()
@@ -125,7 +126,7 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
.sendVolumeKeyEvent(
eq(actionDownVolumeUpKeyEvent),
eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
- eq(true)
+ eq(true),
)
}
@@ -135,7 +136,9 @@ class KeyguardKeyEventInteractorTest : SysuiTestCase() {
whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
- verifyActionUpCollapsesTheShade(KeyEvent.KEYCODE_MENU)
+ val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+ assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+ verify(statusBarKeyguardViewManager).dismissWithAction(any(), isNull(), eq(false))
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 12039c135985..e07961959f1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -118,11 +118,11 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
LOCKSCREEN,
0f,
STARTED,
- ownerName = "KeyguardTransitionRepository(boot)"
+ ownerName = "KeyguardTransitionRepository(boot)",
),
steps[0],
steps[3],
- steps[6]
+ steps[6],
)
)
}
@@ -253,51 +253,20 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
true, // The repo is seeded with a transition from OFF to LOCKSCREEN.
false,
),
- inTransition
+ inTransition,
)
- sendSteps(
- TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, GONE, 0f, STARTED))
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true), inTransition)
- sendSteps(
- TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING))
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true), inTransition)
- sendSteps(
- TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED))
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- false,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true, false), inTransition)
}
@Test
@@ -312,33 +281,16 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
true, // The repo is seeded with a transition from OFF to LOCKSCREEN.
false,
),
- inTransition
+ inTransition,
)
kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Bouncer))
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true), inTransition)
kosmos.setSceneTransition(Idle(Scenes.Bouncer))
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- false,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true, false), inTransition)
}
@Test
@@ -346,14 +298,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
testScope.runTest {
val inTransition by collectValues(underTest.isInTransition)
- assertEquals(
- listOf(
- false,
- true,
- false,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false), inTransition)
// Start FINISHED in GONE.
sendSteps(
@@ -362,32 +307,11 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
)
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- false,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true, false), inTransition)
- sendSteps(
- TransitionStep(GONE, DOZING, 0f, STARTED),
- )
+ sendSteps(TransitionStep(GONE, DOZING, 0f, STARTED))
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- false,
- true,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true, false, true), inTransition)
sendSteps(
TransitionStep(GONE, DOZING, 0.5f, RUNNING),
@@ -410,7 +334,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
// transitioning to GONE, the state we're also FINISHED in.
true,
),
- inTransition
+ inTransition,
)
sendSteps(
@@ -418,18 +342,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED),
)
- assertEquals(
- listOf(
- false,
- true,
- false,
- true,
- false,
- true,
- false,
- ),
- inTransition
- )
+ assertEquals(listOf(false, true, false, true, false, true, false), inTransition)
}
@Test
@@ -440,7 +353,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
collectValues(
underTest.isInTransition(
edge = Edge.create(OFF, OFF),
- edgeWithoutSceneContainer = Edge.create(to = LOCKSCREEN)
+ edgeWithoutSceneContainer = Edge.create(to = LOCKSCREEN),
)
)
@@ -450,49 +363,19 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(AOD, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, RUNNING),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, RUNNING))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, FINISHED),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(LOCKSCREEN, DOZING, 0f, STARTED),
@@ -500,29 +383,14 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(LOCKSCREEN, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
TransitionStep(DOZING, LOCKSCREEN, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -535,33 +403,15 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
kosmos.setSceneTransition(Transition(from = Scenes.Gone, to = Scenes.Lockscreen))
kosmos.setSceneTransition(Idle(Scenes.Lockscreen))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
kosmos.setSceneTransition(Transition(from = Scenes.Lockscreen, to = Scenes.Shade))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
kosmos.setSceneTransition(Idle(Scenes.Shade))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
}
@Test
@@ -575,14 +425,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
kosmos.setSceneTransition(Idle(Scenes.Lockscreen))
kosmos.setSceneTransition(Transition(from = Scenes.Lockscreen, to = Scenes.Gone))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
}
@Test
@@ -602,14 +445,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
kosmos.setSceneTransition(Idle(Scenes.Gone))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
}
@Test
@@ -623,49 +459,19 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(AOD, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, RUNNING),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, RUNNING))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, FINISHED),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(LOCKSCREEN, DOZING, 0f, STARTED),
@@ -673,29 +479,14 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(LOCKSCREEN, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
TransitionStep(DOZING, LOCKSCREEN, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -715,49 +506,19 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(AOD, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
- sendSteps(
- TransitionStep(DOZING, GONE, 0f, STARTED),
- )
+ sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, GONE, 0f, FINISHED),
- )
+ sendSteps(TransitionStep(DOZING, GONE, 0f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
@@ -765,29 +526,14 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(GONE, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -807,48 +553,19 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(AOD, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
- sendSteps(
- TransitionStep(DOZING, GONE, 0f, STARTED),
- )
+ sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, GONE, 0f, RUNNING),
- )
+ sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
- sendSteps(
- TransitionStep(DOZING, GONE, 0f, CANCELED),
- )
+ sendSteps(TransitionStep(DOZING, GONE, 0f, CANCELED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
@@ -856,29 +573,14 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(GONE, DOZING, 1f, FINISHED),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -895,87 +597,43 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
assertThat(results)
.isEqualTo(
listOf(
- false, // Finished in DOZING, not GONE.
+ false // Finished in DOZING, not GONE.
)
)
sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -993,87 +651,43 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
assertThat(results)
.isEqualTo(
listOf(
- false, // Finished in DOZING, not GONE.
+ false // Finished in DOZING, not GONE.
)
)
sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
sendSteps(
TransitionStep(GONE, DOZING, 0f, STARTED),
TransitionStep(GONE, DOZING, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(
TransitionStep(DOZING, GONE, 0f, STARTED),
TransitionStep(DOZING, GONE, 0f, RUNNING),
)
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -1091,72 +705,33 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
assertThat(results)
.isEqualTo(
listOf(
- false, // Finished in DOZING, not GONE.
+ false // Finished in DOZING, not GONE.
)
)
kosmos.setSceneTransition(Transition(from = Scenes.Lockscreen, to = Scenes.Gone))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false))
kosmos.setSceneTransition(Idle(Scenes.Gone))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
kosmos.setSceneTransition(Transition(from = Scenes.Gone, to = Scenes.Lockscreen))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true))
kosmos.setSceneTransition(Idle(Scenes.Lockscreen))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
kosmos.setSceneTransition(Transition(from = Scenes.Lockscreen, to = Scenes.Gone))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false))
kosmos.setSceneTransition(Idle(Scenes.Gone))
- assertThat(results)
- .isEqualTo(
- listOf(
- false,
- true,
- false,
- true,
- )
- )
+ assertThat(results).isEqualTo(listOf(false, true, false, true))
}
@Test
@@ -1165,68 +740,29 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
val currentStates by collectValues(underTest.currentKeyguardState)
// We init the repo with a transition from OFF -> LOCKSCREEN.
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN), currentStates)
- sendSteps(
- TransitionStep(LOCKSCREEN, AOD, 0f, STARTED),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
// The current state should continue to be LOCKSCREEN as we transition to AOD.
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN), currentStates)
- sendSteps(
- TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING))
// The current state should continue to be LOCKSCREEN as we transition to AOD.
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN), currentStates)
- sendSteps(
- TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED))
// Once CANCELED, we're still currently in LOCKSCREEN...
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN), currentStates)
- sendSteps(
- TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED),
- )
+ sendSteps(TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED))
// ...until STARTING back to LOCKSCREEN, at which point the "current" state should be
// the
// one we're transitioning from, despite never FINISHING in that state.
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- AOD,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN, AOD), currentStates)
sendSteps(
TransitionStep(AOD, LOCKSCREEN, 0.8f, RUNNING),
@@ -1234,15 +770,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
)
// FINSHING in LOCKSCREEN should update the current state to LOCKSCREEN.
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- AOD,
- LOCKSCREEN,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN, AOD, LOCKSCREEN), currentStates)
}
@Test
@@ -1251,13 +779,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
val currentStates by collectValues(underTest.currentKeyguardState)
// We init the repo with a transition from OFF -> LOCKSCREEN.
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN), currentStates)
sendSteps(
TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
@@ -1273,7 +795,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
// Transitioned to GONE
GONE,
),
- currentStates
+ currentStates,
)
sendSteps(
@@ -1290,12 +812,10 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
// Current state should not be DOZING until the post-cancelation transition is
// STARTED
),
- currentStates
+ currentStates,
)
- sendSteps(
- TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED),
- )
+ sendSteps(TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED))
assertEquals(
listOf(
@@ -1305,7 +825,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
// DOZING -> LS STARTED, DOZING is now the current state.
DOZING,
),
- currentStates
+ currentStates,
)
sendSteps(
@@ -1313,19 +833,9 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED),
)
- assertEquals(
- listOf(
- OFF,
- LOCKSCREEN,
- GONE,
- DOZING,
- ),
- currentStates
- )
+ assertEquals(listOf(OFF, LOCKSCREEN, GONE, DOZING), currentStates)
- sendSteps(
- TransitionStep(LOCKSCREEN, GONE, 0f, STARTED),
- )
+ sendSteps(TransitionStep(LOCKSCREEN, GONE, 0f, STARTED))
assertEquals(
listOf(
@@ -1336,7 +846,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
// LS -> GONE STARTED, LS is now the current state.
LOCKSCREEN,
),
- currentStates
+ currentStates,
)
sendSteps(
@@ -1354,7 +864,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
// FINISHED in GONE, GONE is now the current state.
GONE,
),
- currentStates
+ currentStates,
)
}
@@ -1504,6 +1014,126 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() {
}
}
+ @Test
+ @EnableSceneContainer
+ fun simulateTransitionStepsForSceneTransitions_emits_correct_values_for_wildcard_from_edge() =
+ testScope.runTest {
+ val sceneToSceneSteps by
+ collectValues(underTest.transition(Edge.create(from = Scenes.Gone)))
+ val progress = MutableSharedFlow<Float>()
+
+ kosmos.setSceneTransition(
+ Transition(Scenes.Gone, Scenes.Lockscreen, progress = progress)
+ )
+
+ progress.emit(0.2f)
+ runCurrent()
+ progress.emit(0.6f)
+ runCurrent()
+
+ kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Bouncer, progress = progress))
+
+ progress.emit(0.1f)
+ runCurrent()
+
+ kosmos.setSceneTransition(
+ Transition(Scenes.Bouncer, Scenes.Lockscreen, progress = progress)
+ )
+
+ progress.emit(0.3f)
+ runCurrent()
+
+ kosmos.setSceneTransition(Idle(Scenes.Gone))
+
+ assertEquals(
+ listOf(
+ TransitionStep(UNDEFINED, UNDEFINED, 0f, STARTED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0.2f, RUNNING),
+ TransitionStep(UNDEFINED, UNDEFINED, 0.6f, RUNNING),
+ TransitionStep(UNDEFINED, UNDEFINED, 1f, FINISHED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0f, STARTED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0.1f, RUNNING),
+ TransitionStep(UNDEFINED, UNDEFINED, 1f, FINISHED),
+ ),
+ sceneToSceneSteps,
+ )
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun simulateTransitionStepsForSceneTransitions_emits_correct_values_for_wildcard_to_edge() =
+ testScope.runTest {
+ val sceneToSceneSteps by
+ collectValues(underTest.transition(Edge.create(to = Scenes.Gone)))
+ val progress = MutableSharedFlow<Float>()
+
+ kosmos.setSceneTransition(
+ Transition(Scenes.Gone, Scenes.Lockscreen, progress = progress)
+ )
+
+ progress.emit(0.2f)
+ runCurrent()
+
+ kosmos.setSceneTransition(Idle(Scenes.Gone))
+
+ kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Bouncer, progress = progress))
+
+ progress.emit(0.1f)
+ runCurrent()
+
+ kosmos.setSceneTransition(Transition(Scenes.Bouncer, Scenes.Gone, progress = progress))
+
+ progress.emit(0.3f)
+ runCurrent()
+
+ kosmos.setSceneTransition(Idle(Scenes.Gone))
+
+ assertEquals(
+ listOf(
+ TransitionStep(UNDEFINED, UNDEFINED, 0f, STARTED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0.3f, RUNNING),
+ TransitionStep(UNDEFINED, UNDEFINED, 1f, FINISHED),
+ ),
+ sceneToSceneSteps,
+ )
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun flatMapLatestWithFinished_emission_of_previous_progress_flow_is_not_interleaving() =
+ testScope.runTest {
+ val sceneToSceneSteps by
+ collectValues(underTest.transition(Edge.create(from = Scenes.Gone)))
+ val progress1 = MutableSharedFlow<Float>()
+ val progress2 = MutableSharedFlow<Float>()
+
+ kosmos.setSceneTransition(
+ Transition(Scenes.Gone, Scenes.Lockscreen, progress = progress1)
+ )
+
+ progress1.emit(0.1f)
+ runCurrent()
+
+ kosmos.setSceneTransition(Transition(Scenes.Gone, Scenes.Bouncer, progress = progress2))
+
+ progress2.emit(0.3f)
+ runCurrent()
+
+ progress1.emit(0.2f)
+ runCurrent()
+
+ assertEquals(
+ listOf(
+ TransitionStep(UNDEFINED, UNDEFINED, 0f, STARTED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0.1f, RUNNING),
+ TransitionStep(UNDEFINED, UNDEFINED, 1f, FINISHED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0f, STARTED),
+ TransitionStep(UNDEFINED, UNDEFINED, 0.3f, RUNNING),
+ ),
+ sceneToSceneSteps,
+ )
+ }
+
private suspend fun sendSteps(vararg steps: TransitionStep) {
steps.forEach {
repository.sendTransitionStep(it)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
index b0e93fbecbb9..b0e93fbecbb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
index 00e79f5a3ac2..00e79f5a3ac2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
index c7da03dbbf30..c7da03dbbf30 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
index de3dc5730421..1d80826d0b45 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt
@@ -28,6 +28,7 @@ import com.android.internal.R
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.asIcon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
@@ -144,13 +145,13 @@ class ModesTileDataInteractorTest : SysuiTestCase() {
// Tile starts with the generic Modes icon.
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
// Add an inactive mode -> Still modes icon
zenModeRepository.addMode(id = "Mode", active = false)
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
// Add an active mode with a default icon: icon should be the mode icon, and the
@@ -158,7 +159,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() {
zenModeRepository.addMode(
id = "Bedtime with default icon",
type = AutomaticZenRule.TYPE_BEDTIME,
- active = true
+ active = true,
)
runCurrent()
assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON)
@@ -189,7 +190,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() {
// Deactivate remaining mode: back to the default modes icon
zenModeRepository.deactivateMode("Driving with custom icon")
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
}
@@ -204,18 +205,18 @@ class ModesTileDataInteractorTest : SysuiTestCase() {
)
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
// Activate a Mode -> Icon doesn't change.
zenModeRepository.addMode(id = "Mode", active = true)
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
zenModeRepository.deactivateMode(id = "Mode")
runCurrent()
- assertThat(tileData?.icon).isEqualTo(MODES_ICON)
+ assertThat(tileData?.icon).isEqualTo(MODES_RESOURCE_ICON)
assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID)
}
@@ -263,7 +264,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() {
val BEDTIME_DRAWABLE = TestStubDrawable("bedtime")
val CUSTOM_DRAWABLE = TestStubDrawable("custom")
- val MODES_ICON = MODES_DRAWABLE.asIcon()
+ val MODES_RESOURCE_ICON = Icon.Resource(MODES_DRAWABLE_ID, null)
val BEDTIME_ICON = BEDTIME_DRAWABLE.asIcon()
val CUSTOM_ICON = CUSTOM_DRAWABLE.asIcon()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
index c3d45dbbd09a..a58cb9ce25b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt
@@ -22,7 +22,9 @@ import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.asIcon
+import com.android.systemui.qs.tiles.ModesTile
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
import com.android.systemui.qs.tiles.viewmodel.QSTileState
@@ -51,6 +53,11 @@ class ModesTileMapperTest : SysuiTestCase() {
.apply {
addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
+ addOverride(
+ ModesTile.ICON_RES_ID,
+ TestStubDrawable(ModesTile.ICON_RES_ID.toString()),
+ )
+ addOverride(123, TestStubDrawable("123"))
}
.resources,
context.theme,
@@ -59,12 +66,7 @@ class ModesTileMapperTest : SysuiTestCase() {
@Test
fun inactiveState() {
val icon = TestStubDrawable("res123").asIcon()
- val model =
- ModesTileModel(
- isActivated = false,
- activeModes = emptyList(),
- icon = icon,
- )
+ val model = ModesTileModel(isActivated = false, activeModes = emptyList(), icon = icon)
val state = underTest.map(config, model)
@@ -76,12 +78,7 @@ class ModesTileMapperTest : SysuiTestCase() {
@Test
fun activeState_oneMode() {
val icon = TestStubDrawable("res123").asIcon()
- val model =
- ModesTileModel(
- isActivated = true,
- activeModes = listOf("DND"),
- icon = icon,
- )
+ val model = ModesTileModel(isActivated = true, activeModes = listOf("DND"), icon = icon)
val state = underTest.map(config, model)
@@ -108,19 +105,36 @@ class ModesTileMapperTest : SysuiTestCase() {
}
@Test
- fun state_modelHasIconResId_includesIconResId() {
- val icon = TestStubDrawable("res123").asIcon()
+ fun resourceIconModel_whenResIdsIdentical_mapsToLoadedIconWithInputResId() {
+ val icon = Icon.Resource(123, null)
val model =
ModesTileModel(
isActivated = false,
activeModes = emptyList(),
icon = icon,
- iconResId = 123
+ iconResId = 123,
)
val state = underTest.map(config, model)
- assertThat(state.icon()).isEqualTo(icon)
+ assertThat(state.icon()).isEqualTo(TestStubDrawable("123").asIcon())
+ assertThat(state.iconRes).isEqualTo(123)
+ }
+
+ @Test
+ fun resourceIconModel_whenResIdsNonIdentical_mapsToLoadedIconWithIconResourceId() {
+ val icon = Icon.Resource(123, null)
+ val model =
+ ModesTileModel(
+ isActivated = false,
+ activeModes = emptyList(),
+ icon = icon,
+ iconResId = 321, // Note: NOT 123. This will be ignored.
+ )
+
+ val state = underTest.map(config, model)
+
+ assertThat(state.icon()).isEqualTo(TestStubDrawable("123").asIcon())
assertThat(state.iconRes).isEqualTo(123)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
index 9331c8df46d4..0bbf47c2275c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
@@ -16,13 +16,16 @@
package com.android.systemui.screenrecord.data.model
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
import com.google.common.truth.Truth.assertThat
+import org.junit.runner.RunWith
import kotlin.test.Test
@SmallTest
+@RunWith(AndroidJUnit4::class)
class ScreenRecordModelTest : SysuiTestCase() {
@Test
fun countdownSeconds_millis0_is0() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
index 4ede90ec4466..4ede90ec4466 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 0e9ef06aa1b9..0454317b5f04 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -22,6 +22,10 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.google.common.truth.Truth.assertThat;
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -36,10 +40,6 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
import android.animation.Animator;
import android.annotation.IdRes;
import android.content.ContentResolver;
@@ -201,6 +201,12 @@ import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.animation.FlingAnimationUtils;
+import dagger.Lazy;
+
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.channels.BufferOverflow;
+import kotlinx.coroutines.test.TestScope;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -215,11 +221,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Optional;
-import dagger.Lazy;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.channels.BufferOverflow;
-import kotlinx.coroutines.test.TestScope;
-
public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -461,7 +462,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
() -> mKosmos.getSceneInteractor(),
() -> mKosmos.getSceneContainerOcclusionInteractor(),
() -> mKosmos.getKeyguardClockInteractor(),
- () -> mKosmos.getSceneBackInteractor());
+ () -> mKosmos.getSceneBackInteractor(),
+ () -> mKosmos.getAlternateBouncerInteractor());
KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -622,7 +624,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
() -> mKosmos.getSceneInteractor(),
() -> mKosmos.getSceneContainerOcclusionInteractor(),
() -> mKosmos.getKeyguardClockInteractor(),
- () -> mKosmos.getSceneBackInteractor()),
+ () -> mKosmos.getSceneBackInteractor(),
+ () -> mKosmos.getAlternateBouncerInteractor()),
mKeyguardBypassController,
mDozeParameters,
mScreenOffAnimationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
index d6b3b919913f..d6b3b919913f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index db274cc311dd..f8720b4fe5f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -28,6 +28,8 @@ import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.givenCanShowAlternateBouncer
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.DisableSceneContainer
@@ -83,8 +85,9 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
- private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+ private val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor }
private val mockDarkAnimator = mock<ObjectAnimator>()
private lateinit var underTest: StatusBarStateControllerImpl
@@ -121,6 +124,7 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
{ kosmos.sceneContainerOcclusionInteractor },
{ kosmos.keyguardClockInteractor },
{ kosmos.sceneBackInteractor },
+ { kosmos.alternateBouncerInteractor },
) {
override fun createDarkAnimator(): ObjectAnimator {
return mockDarkAnimator
@@ -299,6 +303,52 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
@Test
@EnableSceneContainer
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun start_hydratesStatusBarState_withAlternateBouncer() =
+ testScope.runTest {
+ var statusBarState = underTest.state
+ val listener =
+ object : StatusBarStateController.StateListener {
+ override fun onStateChanged(newState: Int) {
+ statusBarState = newState
+ }
+ }
+ underTest.addCallback(listener)
+
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val deviceUnlockStatus by
+ collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+ val alternateBouncerIsVisible by collectLastValue(alternateBouncerInteractor.isVisible)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ assertThat(deviceUnlockStatus!!.isUnlocked).isTrue()
+
+ sceneInteractor.changeScene(toScene = Scenes.Lockscreen, loggingReason = "reason")
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ kosmos.givenCanShowAlternateBouncer()
+ alternateBouncerInteractor.forceShow()
+ runCurrent()
+ assertThat(alternateBouncerIsVisible).isTrue()
+
+ // Call start to begin hydrating based on the scene framework:
+ underTest.start()
+
+ sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason")
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+ }
+
+ @Test
+ @EnableSceneContainer
@EnableFlags(DualShade.FLAG_NAME)
fun start_hydratesStatusBarState_dualShade_whileLocked() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
index 8f41caf54ec8..8f41caf54ec8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 7bc6d4ae2816..7bc6d4ae2816 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
index ecb1a6d44b22..ecb1a6d44b22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index 5005d1609113..5005d1609113 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
index e6101f500ad1..e6101f500ad1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index 77992dbaecc2..77992dbaecc2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index d0c5e7a102e0..d0c5e7a102e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
index c62e40414121..c62e40414121 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
index 118dea6376bb..118dea6376bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
index 6bfb40fa17c5..6bfb40fa17c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
index bfb63ac66d3d..bfb63ac66d3d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index 16101bfe387c..16101bfe387c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
index 325a42bca7d1..325a42bca7d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 791a21d0fb63..791a21d0fb63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
index 4977c548fb92..4977c548fb92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
index 6e4d8863fee2..6e4d8863fee2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 26ce7b956fde..26ce7b956fde 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
index 631120b39805..631120b39805 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
index 01a0fd020bda..01a0fd020bda 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
index cb92b7745961..cb92b7745961 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index d717fe4c1e04..d717fe4c1e04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 1c5f37cc60c3..1c5f37cc60c3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
index 027e899e20df..027e899e20df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 48608ebd6de0..48608ebd6de0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
index 22f1e4604bbd..22f1e4604bbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 2340d0289db4..2340d0289db4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
index ec280a1d6d01..ec280a1d6d01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
index 9d990b1d7edf..9d990b1d7edf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
index f9a9704334a0..f9a9704334a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
index fc829d53a6b4..fc829d53a6b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
index d17c8dbcf38d..d17c8dbcf38d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
index 14bbd38ece2c..14bbd38ece2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
index 660eb308fdf3..660eb308fdf3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index dd03ab393ce9..dd03ab393ce9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 68e17c1b2d73..68e17c1b2d73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
index 0932a0c9307c..0932a0c9307c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
index 7dfdb9228936..7dfdb9228936 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index 83600422bda4..83600422bda4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 1d74331e429b..1d74331e429b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
index b560c591af1e..b560c591af1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index 624c070e95e0..624c070e95e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index bd857807851c..bd857807851c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
index 16061df1fa89..16061df1fa89 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index c5ccf9e6a1d1..74d4178891b9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.domain.interactor
import android.app.AutomaticZenRule
import android.app.Flags
+import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
+import android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY
import android.app.NotificationManager.Policy
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
@@ -25,6 +27,7 @@ import android.provider.Settings.Secure.ZEN_DURATION
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
import android.service.notification.SystemZenRules
+import android.service.notification.ZenPolicy
import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -383,6 +386,120 @@ class ZenModeInteractorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun activeModesBlockingEverything_hasModesWithFilterNone() =
+ testScope.runTest {
+ val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything)
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Filter=None, Not active")
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setActive(false)
+ .build(),
+ TestModeBuilder()
+ .setName("Filter=Priority, Active")
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Filter=None, Active")
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Filter=None, Active Too")
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setActive(true)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active")
+ assertThat(blockingEverything!!.modeNames)
+ .containsExactly("Filter=None, Active", "Filter=None, Active Too")
+ .inOrder()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
+ testScope.runTest {
+ val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia)
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Blocks media, Not active")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setActive(false)
+ .build(),
+ TestModeBuilder()
+ .setName("Allows media, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build())
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Blocks media, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Blocks media, Active Too")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setActive(true)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(blockingMedia!!.mainMode!!.name).isEqualTo("Blocks media, Active")
+ assertThat(blockingMedia!!.modeNames)
+ .containsExactly("Blocks media, Active", "Blocks media, Active Too")
+ .inOrder()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
+ testScope.runTest {
+ val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms)
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Blocks alarms, Not active")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setActive(false)
+ .build(),
+ TestModeBuilder()
+ .setName("Allows alarms, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build())
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Blocks alarms, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Blocks alarms, Active Too")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setActive(true)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active")
+ assertThat(blockingAlarms!!.modeNames)
+ .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too")
+ .inOrder()
+ }
+
+ @Test
@EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
index 06b3b57bd133..06b3b57bd133 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
index 9d93a9c7fe0b..39836e2ce32f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -19,6 +19,7 @@
package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
import android.content.Intent
+import android.content.applicationContext
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -38,9 +39,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.clearInvocations
+import org.mockito.MockitoAnnotations
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -55,14 +58,20 @@ class ModesDialogViewModelTest : SysuiTestCase() {
private val mockDialogDelegate = kosmos.mockModesDialogDelegate
private val mockDialogEventLogger = kosmos.mockModesDialogEventLogger
- private val underTest =
- ModesDialogViewModel(
- context,
- interactor,
- kosmos.testDispatcher,
- mockDialogDelegate,
- mockDialogEventLogger,
- )
+ private lateinit var underTest: ModesDialogViewModel
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest =
+ ModesDialogViewModel(
+ kosmos.applicationContext,
+ interactor,
+ kosmos.testDispatcher,
+ kosmos.mockModesDialogDelegate,
+ kosmos.mockModesDialogEventLogger,
+ )
+ }
@Test
fun tiles_filtersOutUserDisabledModes() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index ba9fa926947c..cd18925eb44f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -25,6 +25,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,10 +35,12 @@ class BackGestureMonitorTest : SysuiTestCase() {
private var gestureState: GestureState = NotStarted
private val gestureMonitor =
- BackGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
- )
+ BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
@Test
fun triggersGestureFinishedForThreeFingerGestureRight() {
@@ -82,7 +85,7 @@ class BackGestureMonitorTest : SysuiTestCase() {
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.processTouchpadEvent(it) }
+ events.forEach { gestureMonitor.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
index a83ed5649889..3f1633a8972f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
@@ -36,10 +36,7 @@ class EasterEggGestureTest : SysuiTestCase() {
private var triggered = false
private val handler =
TouchpadGestureHandler(
- BackGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = {},
- ),
+ BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
EasterEggGestureMonitor(callback = { triggered = true }),
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
index 59cc026e82ee..edf0e5698bf0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
@@ -25,6 +25,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,10 +35,12 @@ class HomeGestureMonitorTest : SysuiTestCase() {
private var gestureState: GestureState = GestureState.NotStarted
private val gestureMonitor =
- HomeGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
- )
+ HomeGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
@Test
fun triggersGestureFinishedForThreeFingerGestureUp() {
@@ -78,7 +81,7 @@ class HomeGestureMonitorTest : SysuiTestCase() {
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.processTouchpadEvent(it) }
+ events.forEach { gestureMonitor.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
index 7eac6bb09264..f68e7732d04e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
@@ -26,6 +26,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
@@ -44,7 +45,7 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() {
}
private var gestureState: GestureState = GestureState.NotStarted
- private val velocityTracker =
+ private val velocityTracker1D =
mock<VelocityTracker1D> {
// by default return correct speed for the gesture - as if pointer is slowing down
on { calculateVelocity() } doReturn SLOW
@@ -52,11 +53,15 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() {
private val gestureMonitor =
RecentAppsGestureMonitor(
gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
velocityThresholdPxPerMs = THRESHOLD_VELOCITY_PX_PER_MS,
- velocityTracker = velocityTracker,
+ velocityTracker = VerticalVelocityTracker(velocityTracker1D),
)
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
+
@Test
fun triggersGestureFinishedForThreeFingerGestureUp() {
assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Finished)
@@ -64,7 +69,7 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() {
@Test
fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() {
- whenever(velocityTracker.calculateVelocity()).thenReturn(FAST)
+ whenever(velocityTracker1D.calculateVelocity()).thenReturn(FAST)
assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted)
}
@@ -102,7 +107,7 @@ class RecentAppsGestureMonitorTest : SysuiTestCase() {
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.processTouchpadEvent(it) }
+ events.forEach { gestureMonitor.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 4d26366614f6..9f7ea679b822 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,14 +36,14 @@ import org.junit.runner.RunWith
class TouchpadGestureHandlerTest : SysuiTestCase() {
private var gestureState: GestureState = GestureState.NotStarted
- private val handler =
- TouchpadGestureHandler(
- BackGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
- ),
- EasterEggGestureMonitor {},
- )
+ private val gestureMonitor =
+ BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+ private val handler = TouchpadGestureHandler(gestureMonitor, EasterEggGestureMonitor {})
+
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
@Test
fun handlesEventsFromTouchpad() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
index 7ce421a5aa62..06a3e8b0a766 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
@@ -27,7 +27,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.fakeVolumeDialogController
import com.android.systemui.testKosmos
import com.android.systemui.volume.Events
-import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
index fa7f37c7ba16..449dc20656b7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
@@ -16,11 +16,16 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+import android.content.mockedContext
+import android.content.packageManager
+import android.content.pm.PackageManager.FEATURE_PC
import android.graphics.drawable.TestStubDrawable
import android.media.AudioManager
+import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.media.flags.Flags;
import com.android.settingslib.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -42,6 +47,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
private const val builtInDeviceName = "This phone"
@@ -79,6 +85,8 @@ class MediaOutputComponentInteractorTest : SysuiTestCase() {
fun inCall_stateIs_Calling() =
with(kosmos) {
testScope.runTest {
+ whenever(mockedContext.getPackageManager()).thenReturn(packageManager)
+ whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(false)
with(audioRepository) {
setMode(AudioManager.MODE_IN_CALL)
setCommunicationDevice(TestAudioDevicesFactory.builtInDevice())
@@ -98,6 +106,33 @@ class MediaOutputComponentInteractorTest : SysuiTestCase() {
}
}
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ fun inCall_stateIs_Calling_enableInputRouting_desktop() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(mockedContext.getPackageManager()).thenReturn(packageManager)
+ whenever(packageManager.hasSystemFeature(FEATURE_PC)).thenReturn(true)
+
+ with(audioRepository) {
+ setMode(AudioManager.MODE_IN_CALL)
+ setCommunicationDevice(TestAudioDevicesFactory.builtInDevice())
+ }
+
+ val model by collectLastValue(underTest.mediaOutputModel.filterData())
+ runCurrent()
+
+ assertThat(model)
+ .isEqualTo(
+ MediaOutputComponentModel.Calling(
+ device = AudioOutputDevice.BuiltIn(builtInDeviceName, testIcon),
+ isInAudioSharing = false,
+ canOpenAudioSwitcher = true,
+ )
+ )
+ }
+ }
+
@Test
fun hasSession_stateIs_MediaSession() =
with(kosmos) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
new file mode 100644
index 000000000000..f80b36a10dc2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
+
+import android.app.Flags
+import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
+import android.media.AudioManager
+import android.platform.test.annotations.EnableFlags
+import android.service.notification.ZenPolicy
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
+import com.android.systemui.volume.shared.volumePanelLogger
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AudioStreamSliderViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val zenModeRepository = kosmos.fakeZenModeRepository
+
+ private lateinit var mediaStream: AudioStreamSliderViewModel
+ private lateinit var alarmsStream: AudioStreamSliderViewModel
+ private lateinit var notificationStream: AudioStreamSliderViewModel
+ private lateinit var otherStream: AudioStreamSliderViewModel
+
+ @Before
+ fun setUp() {
+ mediaStream = audioStreamSliderViewModel(AudioManager.STREAM_MUSIC)
+ alarmsStream = audioStreamSliderViewModel(AudioManager.STREAM_ALARM)
+ notificationStream = audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION)
+ otherStream = audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL)
+ }
+
+ private fun audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
+ return AudioStreamSliderViewModel(
+ AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
+ testScope.backgroundScope,
+ context,
+ kosmos.audioVolumeInteractor,
+ kosmos.zenModeInteractor,
+ kosmos.uiEventLogger,
+ kosmos.volumePanelLogger,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
+ fun slider_media_hasDisabledByModesText() =
+ testScope.runTest {
+ val mediaSlider by collectLastValue(mediaStream.slider)
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setName("Media is ok")
+ .setZenPolicy(ZenPolicy.Builder().allowAllSounds().build())
+ .setActive(true)
+ .build()
+ )
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setName("No media plz")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setActive(true)
+ .build()
+ )
+ runCurrent()
+
+ assertThat(mediaSlider!!.disabledMessage)
+ .isEqualTo("Unavailable because No media plz is on")
+
+ zenModeRepository.clearModes()
+ runCurrent()
+
+ assertThat(mediaSlider!!.disabledMessage).isEqualTo("Unavailable")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
+ fun slider_alarms_hasDisabledByModesText() =
+ testScope.runTest {
+ val alarmsSlider by collectLastValue(alarmsStream.slider)
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setName("Alarms are ok")
+ .setZenPolicy(ZenPolicy.Builder().allowAllSounds().build())
+ .setActive(true)
+ .build()
+ )
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setName("Zzzzz")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setActive(true)
+ .build()
+ )
+ runCurrent()
+
+ assertThat(alarmsSlider!!.disabledMessage).isEqualTo("Unavailable because Zzzzz is on")
+
+ zenModeRepository.clearModes()
+ runCurrent()
+
+ assertThat(alarmsSlider!!.disabledMessage).isEqualTo("Unavailable")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
+ fun slider_other_hasDisabledByModesText() =
+ testScope.runTest {
+ val otherSlider by collectLastValue(otherStream.slider)
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setName("Everything blocked")
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setActive(true)
+ .build()
+ )
+ runCurrent()
+
+ assertThat(otherSlider!!.disabledMessage)
+ .isEqualTo("Unavailable because Everything blocked is on")
+
+ zenModeRepository.clearModes()
+ runCurrent()
+
+ assertThat(otherSlider!!.disabledMessage).isEqualTo("Unavailable")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
+ fun slider_notification_hasSpecialDisabledText() =
+ testScope.runTest {
+ val notificationSlider by collectLastValue(notificationStream.slider)
+ runCurrent()
+
+ assertThat(notificationSlider!!.disabledMessage)
+ .isEqualTo("Unavailable because ring is muted")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index b8dd334dcad9..b8dd334dcad9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index b1736b16875d..c09509d8690a 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -14,7 +14,6 @@
package com.android.systemui.plugins;
-import android.annotation.IntegerRes;
import android.content.ComponentName;
import android.media.AudioManager;
import android.media.AudioSystem;
@@ -22,6 +21,8 @@ import android.os.Handler;
import android.os.VibrationEffect;
import android.util.SparseArray;
+import androidx.annotation.StringRes;
+
import com.android.systemui.plugins.VolumeDialogController.Callbacks;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.plugins.VolumeDialogController.StreamState;
@@ -90,7 +91,7 @@ public interface VolumeDialogController {
public int levelMax;
public boolean muted;
public boolean muteSupported;
- public @IntegerRes int name;
+ public @StringRes int name;
public String remoteLabel;
public boolean routedToBluetooth;
diff --git a/packages/SystemUI/res/drawable/volume_dialog_background.xml b/packages/SystemUI/res/drawable/volume_dialog_background.xml
new file mode 100644
index 000000000000..7d7498feeba6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/volume_dialog_background_corner_radius" />
+ <solid android:color="?androidprv:attr/materialColorSurface" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml
new file mode 100644
index 000000000000..2694435bcc78
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="20dp" />
+ <solid android:color="?androidprv:attr/colorSurface" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml
new file mode 100644
index 000000000000..66a205a57c61
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size
+ android:width="@dimen/volume_dialog_floating_sliders_spacing"
+ android:height="@dimen/volume_dialog_floating_sliders_spacing" />
+ <solid android:color="@color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml
new file mode 100644
index 000000000000..3c60784cf6b6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size
+ android:width="@dimen/volume_dialog_spacing"
+ android:height="@dimen/volume_dialog_spacing" />
+ <solid android:color="@color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
index 21b177bead11..fa06bd6abeea 100644
--- a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
+++ b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
@@ -22,7 +22,7 @@
android:autoMirrored="true">
<item android:id="@+id/volume_seekbar_progress_solid">
<shape>
- <size android:height="@dimen/volume_dialog_slider_width" />
+ <size android:height="@dimen/volume_dialog_slider_width_legacy" />
<solid android:color="?android:attr/colorAccent" />
<corners android:radius="@dimen/volume_dialog_slider_corner_radius"/>
</shape>
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog.xml b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
index 0fbc519ca8dd..f77db956a493 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
@@ -1,92 +1,67 @@
<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@android:color/transparent"
+ android:layout_gravity="right"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning"
android:theme="@style/volume_dialog_theme">
- <FrameLayout
- android:id="@+id/volume_dialog"
+ <LinearLayout
+ android:id="@+id/volume_dialog_floating_sliders_container"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="right"
- android:background="@android:color/transparent"
- android:padding="@dimen/volume_dialog_panel_transparent_padding"
- android:clipToPadding="false">
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="right"
- android:orientation="vertical"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:background="@android:color/transparent">
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
- <LinearLayout
- android:id="@+id/volume_dialog_rows"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="horizontal"
- android:background="@drawable/tv_volume_dialog_background">
- <!-- volume rows added and removed here! :-) -->
- </LinearLayout>
-
- </LinearLayout>
+ <LinearLayout
+ android:layout_width="@dimen/volume_dialog_width"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
<FrameLayout
- android:id="@+id/odi_captions"
- android:layout_width="@dimen/volume_dialog_caption_size"
- android:layout_height="@dimen/volume_dialog_caption_size"
- android:layout_marginRight="68dp"
- android:layout_gravity="right"
- android:clipToPadding="false"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:background="@drawable/rounded_bg_full">
-
- <com.android.systemui.volume.CaptionsToggleImageButton
- android:id="@+id/odi_captions_icon"
- android:src="@drawable/ic_volume_odi_captions_disabled"
- style="@style/VolumeButtons"
- android:background="@drawable/rounded_ripple"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tint="@color/caption_tint_color_selector"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false"/>
-
- </FrameLayout>
-
- <ViewStub
- android:id="@+id/odi_captions_tooltip_stub"
- android:inflatedId="@+id/odi_captions_tooltip_view"
- android:layout="@layout/volume_tool_tip_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="@dimen/volume_tool_tip_right_margin"
- android:layout_marginTop="@dimen/volume_tool_tip_top_margin"
- android:layout_gravity="right"/>
+ android:id="@+id/volume_dialog_ringer_button"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size" />
- </FrameLayout>
+ <include
+ android:id="@+id/volume_dialog_slider"
+ layout="@layout/volume_dialog_slider" />
-</FrameLayout>
+ <Button
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml
new file mode 100644
index 000000000000..0fbc519ca8dd
--- /dev/null
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml
@@ -0,0 +1,92 @@
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/volume_dialog_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:theme="@style/volume_dialog_theme">
+
+ <FrameLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:background="@android:color/transparent"
+ android:padding="@dimen/volume_dialog_panel_transparent_padding"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:orientation="vertical"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:background="@android:color/transparent">
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:background="@drawable/tv_volume_dialog_background">
+ <!-- volume rows added and removed here! :-) -->
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/odi_captions"
+ android:layout_width="@dimen/volume_dialog_caption_size"
+ android:layout_height="@dimen/volume_dialog_caption_size"
+ android:layout_marginRight="68dp"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:background="@drawable/rounded_bg_full">
+
+ <com.android.systemui.volume.CaptionsToggleImageButton
+ android:id="@+id/odi_captions_icon"
+ android:src="@drawable/ic_volume_odi_captions_disabled"
+ style="@style/VolumeButtons"
+ android:background="@drawable/rounded_ripple"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tint="@color/caption_tint_color_selector"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false"/>
+
+ </FrameLayout>
+
+ <ViewStub
+ android:id="@+id/odi_captions_tooltip_stub"
+ android:inflatedId="@+id/odi_captions_tooltip_view"
+ android:layout="@layout/volume_tool_tip_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/volume_tool_tip_right_margin"
+ android:layout_marginTop="@dimen/volume_tool_tip_top_margin"
+ android:layout_gravity="right"/>
+
+ </FrameLayout>
+
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
index cf301c96a9f8..eb89489bdd83 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
@@ -63,8 +63,8 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:layoutDirection="ltr"
- android:maxHeight="@dimen/volume_dialog_slider_width"
- android:minHeight="@dimen/volume_dialog_slider_width"
+ android:maxHeight="@dimen/volume_dialog_slider_width_legacy"
+ android:minHeight="@dimen/volume_dialog_slider_width_legacy"
android:progressDrawable="@drawable/volume_row_seekbar"
android:thumb="@drawable/tv_volume_row_seek_thumb"
android:splitTrack="false"
diff --git a/packages/SystemUI/res/layout-land/volume_dialog.xml b/packages/SystemUI/res/layout-land/volume_dialog.xml
index 08edf59000b8..f77db956a493 100644
--- a/packages/SystemUI/res/layout-land/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land/volume_dialog.xml
@@ -1,146 +1,67 @@
<!--
- ~ Copyright (C) 2019 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="right"
android:layout_gravity="right"
- android:background="@android:color/transparent"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning"
android:theme="@style/volume_dialog_theme">
- <!-- right-aligned to be physically near volume button -->
<LinearLayout
- android:id="@+id/volume_dialog"
+ android:id="@+id/volume_dialog_floating_sliders_container"
android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
+
+ <LinearLayout
+ android:layout_width="@dimen/volume_dialog_width"
android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
android:orientation="vertical"
- android:clipToPadding="false"
- android:clipChildren="false">
-
-
- <LinearLayout
- android:id="@+id/volume_dialog_top_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:clipChildren="false"
- android:gravity="right">
-
- <include layout="@layout/volume_ringer_drawer" />
-
- <FrameLayout
- android:visibility="gone"
- android:id="@+id/ringer"
- android:layout_width="@dimen/volume_dialog_ringer_size"
- android:layout_height="@dimen/volume_dialog_ringer_size"
- android:layout_marginBottom="@dimen/volume_dialog_spacer"
- android:gravity="right"
- android:layout_gravity="right"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:clipToPadding="false"
- android:background="@drawable/rounded_bg_full">
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/ringer_icon"
- style="@style/VolumeButtons"
- android:background="@drawable/rounded_ripple"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="fitCenter"
- android:padding="@dimen/volume_dialog_ringer_icon_padding"
- android:tint="?android:attr/textColorPrimary"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:orientation="vertical"
- android:clipChildren="false"
- android:clipToPadding="false" >
- <LinearLayout
- android:id="@+id/volume_dialog_rows"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="horizontal">
- <!-- volume rows added and removed here! :-) -->
- </LinearLayout>
- <FrameLayout
- android:id="@+id/settings_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/volume_background_bottom"
- android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
-
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/settings"
- android:layout_width="@dimen/volume_dialog_tap_target_size"
- android:layout_height="@dimen/volume_dialog_tap_target_size"
- android:layout_gravity="center"
- android:background="@drawable/ripple_drawable_20dp"
- android:contentDescription="@string/accessibility_volume_settings"
- android:scaleType="centerInside"
- android:soundEffectsEnabled="false"
- android:src="@drawable/horizontal_ellipsis"
- android:tint="?androidprv:attr/colorAccent" />
- </FrameLayout>
- </LinearLayout>
-
- </LinearLayout>
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
<FrameLayout
- android:id="@+id/odi_captions"
- android:layout_width="@dimen/volume_dialog_caption_size"
- android:layout_height="@dimen/volume_dialog_caption_size"
- android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
- android:gravity="right"
- android:layout_gravity="right"
- android:clipToPadding="false"
- android:clipToOutline="true"
- android:background="@drawable/volume_row_rounded_background">
- <com.android.systemui.volume.CaptionsToggleImageButton
- android:id="@+id/odi_captions_icon"
- android:src="@drawable/ic_volume_odi_captions_disabled"
- style="@style/VolumeButtons"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tint="?android:attr/colorAccent"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
- </LinearLayout>
+ android:id="@+id/volume_dialog_ringer_button"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size" />
- <ViewStub
- android:id="@+id/odi_captions_tooltip_stub"
- android:inflatedId="@+id/odi_captions_tooltip_view"
- android:layout="@layout/volume_tool_tip_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom | right"
- android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+ <include
+ android:id="@+id/volume_dialog_slider"
+ layout="@layout/volume_dialog_slider" />
-</FrameLayout> \ No newline at end of file
+ <Button
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml
new file mode 100644
index 000000000000..08edf59000b8
--- /dev/null
+++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml
@@ -0,0 +1,146 @@
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/volume_dialog_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:background="@android:color/transparent"
+ android:theme="@style/volume_dialog_theme">
+
+ <!-- right-aligned to be physically near volume button -->
+ <LinearLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:orientation="vertical"
+ android:clipToPadding="false"
+ android:clipChildren="false">
+
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_top_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:clipChildren="false"
+ android:gravity="right">
+
+ <include layout="@layout/volume_ringer_drawer" />
+
+ <FrameLayout
+ android:visibility="gone"
+ android:id="@+id/ringer"
+ android:layout_width="@dimen/volume_dialog_ringer_size"
+ android:layout_height="@dimen/volume_dialog_ringer_size"
+ android:layout_marginBottom="@dimen/volume_dialog_spacer"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:clipToPadding="false"
+ android:background="@drawable/rounded_bg_full">
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/ringer_icon"
+ style="@style/VolumeButtons"
+ android:background="@drawable/rounded_ripple"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter"
+ android:padding="@dimen/volume_dialog_ringer_icon_padding"
+ android:tint="?android:attr/textColorPrimary"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:orientation="vertical"
+ android:clipChildren="false"
+ android:clipToPadding="false" >
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+ <!-- volume rows added and removed here! :-) -->
+ </LinearLayout>
+ <FrameLayout
+ android:id="@+id/settings_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_background_bottom"
+ android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/settings"
+ android:layout_width="@dimen/volume_dialog_tap_target_size"
+ android:layout_height="@dimen/volume_dialog_tap_target_size"
+ android:layout_gravity="center"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:scaleType="centerInside"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/colorAccent" />
+ </FrameLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/odi_captions"
+ android:layout_width="@dimen/volume_dialog_caption_size"
+ android:layout_height="@dimen/volume_dialog_caption_size"
+ android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:clipToOutline="true"
+ android:background="@drawable/volume_row_rounded_background">
+ <com.android.systemui.volume.CaptionsToggleImageButton
+ android:id="@+id/odi_captions_icon"
+ android:src="@drawable/ic_volume_odi_captions_disabled"
+ style="@style/VolumeButtons"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tint="?android:attr/colorAccent"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+ </LinearLayout>
+
+ <ViewStub
+ android:id="@+id/odi_captions_tooltip_stub"
+ android:inflatedId="@+id/odi_captions_tooltip_view"
+ android:layout="@layout/volume_tool_tip_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom | right"
+ android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/audio_sharing_dialog.xml b/packages/SystemUI/res/layout/audio_sharing_dialog.xml
new file mode 100644
index 000000000000..7534e159beb0
--- /dev/null
+++ b/packages/SystemUI/res/layout/audio_sharing_dialog.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ style="@style/Widget.SliceView.Panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="28dp"
+ android:layout_height="28dp"
+ android:src="@drawable/ic_bt_le_audio_sharing"
+ android:layout_marginTop="5dp"
+ android:layout_marginBottom="20dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/title"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:gravity="center_vertical|center_horizontal"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_title"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ android:textSize="24sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/subtitle"
+ app:layout_constraintTop_toBottomOf="@id/icon" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:gravity="center_vertical|center_horizontal"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ android:textFontWeight="500"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/message"
+ app:layout_constraintTop_toBottomOf="@id/title" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:gravity="center_vertical|center_horizontal"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_message"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/share_audio_button"
+ app:layout_constraintTop_toBottomOf="@id/subtitle" />
+
+ <Button
+ android:id="@+id/share_audio_button"
+ style="@style/SettingsLibActionButton"
+ android:textColor="?androidprv:attr/textColorOnAccent"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:layout_marginBottom="4dp"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="64dp"
+ android:contentDescription="@string/accessibility_bluetooth_device_settings_see_all"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/message"
+ app:layout_constraintBottom_toTopOf="@+id/switch_active_button"
+ android:text="@string/quick_settings_bluetooth_audio_sharing_button"
+ android:maxLines="2" />
+
+ <Button
+ android:id="@+id/switch_active_button"
+ style="@style/SettingsLibActionButton"
+ android:textColor="?androidprv:attr/textColorOnAccent"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:layout_marginBottom="20dp"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="64dp"
+ android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/share_audio_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:maxLines="2" />
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 39a1f1f9b85d..f77db956a493 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2015 The Android Open Source Project
+ Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,133 +13,55 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:sysui="http://schemas.android.com/apk/res-auto"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="right"
android:layout_gravity="right"
- android:clipToPadding="false"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning"
android:theme="@style/volume_dialog_theme">
- <!-- right-aligned to be physically near volume button -->
<LinearLayout
- android:id="@+id/volume_dialog"
+ android:id="@+id/volume_dialog_floating_sliders_container"
android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
+
+ <LinearLayout
+ android:layout_width="@dimen/volume_dialog_width"
android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
android:orientation="vertical"
- android:clipToPadding="false"
- android:clipChildren="false">
-
- <LinearLayout
- android:id="@+id/volume_dialog_top_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:clipChildren="false"
- android:orientation="vertical"
- android:gravity="right">
-
- <include layout="@layout/volume_ringer_drawer" />
-
- <FrameLayout
- android:visibility="gone"
- android:id="@+id/ringer"
- android:layout_width="@dimen/volume_dialog_ringer_size"
- android:layout_height="@dimen/volume_dialog_ringer_size"
- android:layout_marginBottom="@dimen/volume_dialog_spacer"
- android:gravity="right"
- android:layout_gravity="right"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:clipToPadding="false"
- android:background="@drawable/rounded_bg_full">
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/ringer_icon"
- style="@style/VolumeButtons"
- android:background="@drawable/rounded_ripple"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="fitCenter"
- android:padding="@dimen/volume_dialog_ringer_icon_padding"
- android:tint="?android:attr/textColorPrimary"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:orientation="vertical"
- android:clipChildren="false"
- android:clipToPadding="false" >
- <LinearLayout
- android:id="@+id/volume_dialog_rows"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="horizontal">
- <!-- volume rows added and removed here! :-) -->
- </LinearLayout>
- <FrameLayout
- android:id="@+id/settings_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/volume_background_bottom"
- android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/settings"
- android:src="@drawable/horizontal_ellipsis"
- android:layout_width="@dimen/volume_dialog_tap_target_size"
- android:layout_height="@dimen/volume_dialog_tap_target_size"
- android:layout_gravity="center"
- android:contentDescription="@string/accessibility_volume_settings"
- android:background="@drawable/ripple_drawable_20dp"
- android:tint="?androidprv:attr/colorAccent"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
- </LinearLayout>
-
- </LinearLayout>
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
<FrameLayout
- android:id="@+id/odi_captions"
- android:layout_width="@dimen/volume_dialog_caption_size"
- android:layout_height="@dimen/volume_dialog_caption_size"
- android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
- android:gravity="right"
- android:layout_gravity="right"
- android:clipToPadding="false"
- android:clipToOutline="true"
- android:background="@drawable/volume_row_rounded_background">
- <com.android.systemui.volume.CaptionsToggleImageButton
- android:id="@+id/odi_captions_icon"
- android:src="@drawable/ic_volume_odi_captions_disabled"
- style="@style/VolumeButtons"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tint="?android:attr/colorAccent"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false"/>
- </FrameLayout>
- </LinearLayout>
+ android:id="@+id/volume_dialog_ringer_button"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size" />
- <ViewStub
- android:id="@+id/odi_captions_tooltip_stub"
- android:inflatedId="@+id/odi_captions_tooltip_view"
- android:layout="@layout/volume_tool_tip_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom | right"
- android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+ <include
+ android:id="@+id/volume_dialog_slider"
+ layout="@layout/volume_dialog_slider" />
-</FrameLayout> \ No newline at end of file
+ <Button
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml
new file mode 100644
index 000000000000..39a1f1f9b85d
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml
@@ -0,0 +1,145 @@
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/volume_dialog_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:theme="@style/volume_dialog_theme">
+
+ <!-- right-aligned to be physically near volume button -->
+ <LinearLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:orientation="vertical"
+ android:clipToPadding="false"
+ android:clipChildren="false">
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_top_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:orientation="vertical"
+ android:gravity="right">
+
+ <include layout="@layout/volume_ringer_drawer" />
+
+ <FrameLayout
+ android:visibility="gone"
+ android:id="@+id/ringer"
+ android:layout_width="@dimen/volume_dialog_ringer_size"
+ android:layout_height="@dimen/volume_dialog_ringer_size"
+ android:layout_marginBottom="@dimen/volume_dialog_spacer"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:clipToPadding="false"
+ android:background="@drawable/rounded_bg_full">
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/ringer_icon"
+ style="@style/VolumeButtons"
+ android:background="@drawable/rounded_ripple"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter"
+ android:padding="@dimen/volume_dialog_ringer_icon_padding"
+ android:tint="?android:attr/textColorPrimary"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:orientation="vertical"
+ android:clipChildren="false"
+ android:clipToPadding="false" >
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+ <!-- volume rows added and removed here! :-) -->
+ </LinearLayout>
+ <FrameLayout
+ android:id="@+id/settings_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_background_bottom"
+ android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/settings"
+ android:src="@drawable/horizontal_ellipsis"
+ android:layout_width="@dimen/volume_dialog_tap_target_size"
+ android:layout_height="@dimen/volume_dialog_tap_target_size"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:tint="?androidprv:attr/colorAccent"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/odi_captions"
+ android:layout_width="@dimen/volume_dialog_caption_size"
+ android:layout_height="@dimen/volume_dialog_caption_size"
+ android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:clipToOutline="true"
+ android:background="@drawable/volume_row_rounded_background">
+ <com.android.systemui.volume.CaptionsToggleImageButton
+ android:id="@+id/odi_captions_icon"
+ android:src="@drawable/ic_volume_odi_captions_disabled"
+ style="@style/VolumeButtons"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tint="?android:attr/colorAccent"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false"/>
+ </FrameLayout>
+ </LinearLayout>
+
+ <ViewStub
+ android:id="@+id/odi_captions_tooltip_stub"
+ android:inflatedId="@+id/odi_captions_tooltip_view"
+ android:layout="@layout/volume_tool_tip_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom | right"
+ android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
new file mode 100644
index 000000000000..8acdd39faaa2
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/volume_dialog_slider_width"
+ android:layout_height="@dimen/volume_dialog_slider_height">
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/volume_dialog_slider"
+ android:layout_width="@dimen/volume_dialog_slider_height"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:rotation="270"
+ android:theme="@style/Theme.MaterialComponents.DayNight" />
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml
new file mode 100644
index 000000000000..db800aa4a873
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_dialog_floating_slider_background"
+ android:paddingHorizontal="@dimen/volume_dialog_floating_sliders_horizontal_padding"
+ android:paddingVertical="@dimen/volume_dialog_floating_sliders_vertical_padding">
+
+ <include layout="@layout/volume_dialog_slider" />
+</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1727a5fec0a2..6c8a7403953e 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -591,7 +591,7 @@
<dimen name="volume_dialog_panel_width_half">28dp</dimen>
- <dimen name="volume_dialog_slider_width">42dp</dimen>
+ <dimen name="volume_dialog_slider_width_legacy">42dp</dimen>
<dimen name="volume_dialog_slider_corner_radius">21dp</dimen>
@@ -622,10 +622,6 @@
<dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen>
- <!-- Volume panel slices dimensions -->
- <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
- <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
-
<dimen name="bottom_sheet_corner_radius">28dp</dimen>
<!-- Size of each item in the ringer selector drawer. -->
@@ -2050,4 +2046,22 @@
<dimen name="contextual_edu_dialog_bottom_margin">80dp</dimen>
<dimen name="contextual_edu_dialog_elevation">2dp</dimen>
+
+ <!-- Volume start -->
+ <dimen name="volume_dialog_background_corner_radius">30dp</dimen>
+ <dimen name="volume_dialog_width">60dp</dimen>
+ <dimen name="volume_dialog_vertical_padding">6dp</dimen>
+ <dimen name="volume_dialog_components_spacing">8dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_horizontal_padding">4dp</dimen>
+ <dimen name="volume_dialog_spacing">4dp</dimen>
+ <dimen name="volume_dialog_button_size">48dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_bottom_padding">48dp</dimen>
+ <dimen name="volume_dialog_slider_width">52dp</dimen>
+ <dimen name="volume_dialog_slider_height">254dp</dimen>
+
+ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
+ <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
+ <!-- Volume end -->
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 96a85d78e2b5..2c5fb5667db7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -765,6 +765,14 @@
<string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing audio</string>
<!-- QuickSettings: Bluetooth dialog audio sharing button text accessibility label. Used as part of the string "Double tap to enter audio sharing settings". [CHAR LIMIT=50]-->
<string name="quick_settings_bluetooth_audio_sharing_button_accessibility">enter audio sharing settings</string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog message. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_message">This device\'s music and videos will play on both pairs of headphones</string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog title. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_title">Share your audio</string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog subtitle. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_subtitle"><xliff:g id="available_device_name" example="device 1">%1$s</xliff:g> and <xliff:g id="active_device_name" example="device 2">%2$s</xliff:g></string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog button text. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_switch_to_button">Switch to <xliff:g id="available_device_name" example="device 1">%1$s</xliff:g></string>
<!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
<string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
@@ -1746,6 +1754,11 @@
<!-- A message shown when the media volume changing is disabled because of the don't disturb mode [CHAR_LIMIT=50]-->
<string name="stream_media_unavailable">Unavailable because Do Not Disturb is on</string>
+ <!-- A message shown when a specific volume (e.g. Alarms, Media, etc) is disabled because an active mode is muting that audio stream altogether [CHAR_LIMIT=50]-->
+ <string name="stream_unavailable_by_modes">Unavailable because <xliff:g id="mode" example="Bedtime">%s</xliff:g> is on</string>
+ <!-- A message shown when a specific volume (e.g. Alarms, Media, etc) is disabled but we don't know which mode (or anything else) is responsible. [CHAR_LIMIT=50]-->
+ <string name="stream_unavailable_by_unknown">Unavailable</string>
+
<!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on vibrate. [CHAR_LIMIT=NONE] -->
<!-- Shown in the header of quick settings to indicate to the user that their phone ringer is on silent (muted). [CHAR_LIMIT=NONE] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 431f04882c8d..83ab5245bc31 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1875,7 +1875,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
if (posture == DEVICE_POSTURE_OPENED) {
mLogger.d("Posture changed to open - attempting to request active"
+ " unlock and run face auth");
- getFaceAuthInteractor().onDeviceUnfolded();
+ if (getFaceAuthInteractor() != null) {
+ getFaceAuthInteractor().onDeviceUnfolded();
+ }
requestActiveUnlockFromWakeReason(PowerManager.WAKE_REASON_UNFOLD_DEVICE,
false);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index 831543da3237..ef172a1b24f6 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -69,7 +69,9 @@ public abstract class ClockRegistryModule {
layoutInflater,
resources,
featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION),
- MigrateClocksToBlueprint.isEnabled()),
+ MigrateClocksToBlueprint.isEnabled(),
+ com.android.systemui.Flags.clockReactiveVariants()
+ ),
context.getString(R.string.lockscreen_clock_id_fallback),
clockBuffers,
/* keepAllLoaded = */ false,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
index 275147e6694c..41b9d3372392 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java
@@ -226,7 +226,11 @@ public class AccessibilityFloatingMenuController implements
mBtnTargets =
mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets();
mHandler.post(
- () -> handleFloatingMenuVisibility(mIsKeyguardVisible, mBtnMode, mBtnTargets));
+ () -> {
+ // Force a refresh by destroying the menu if it exists.
+ destroyFloatingMenu();
+ handleFloatingMenuVisibility(mIsKeyguardVisible, mBtnMode, mBtnTargets);
+ });
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index e0f73a63113a..cbdb8827e39c 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -319,7 +319,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon
if (item == null && active) {
item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime());
if (isOpMicrophone(code)) {
- item.setDisabled(isAnyRecordingPausedLocked(uid));
+ item.setDisabled(isAllRecordingPausedLocked(uid));
} else if (isOpCamera(code)) {
item.setDisabled(mCameraDisabled);
}
@@ -521,18 +521,21 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon
}
- private boolean isAnyRecordingPausedLocked(int uid) {
+ // TODO(b/365843152) remove AudioRecordingConfiguration listening
+ private boolean isAllRecordingPausedLocked(int uid) {
if (mMicMuted) {
return true;
}
List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid);
if (configs == null) return false;
+ // If we are aware of AudioRecordConfigs, suppress the indicator if all of them are known
+ // to be silenced.
int configsNum = configs.size();
for (int i = 0; i < configsNum; i++) {
AudioRecordingConfiguration config = configs.get(i);
- if (config.isClientSilenced()) return true;
+ if (!config.isClientSilenced()) return false;
}
- return false;
+ return true;
}
private void updateSensorDisabledStatus() {
@@ -543,7 +546,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon
boolean paused = false;
if (isOpMicrophone(item.getCode())) {
- paused = isAnyRecordingPausedLocked(item.getUid());
+ paused = isAllRecordingPausedLocked(item.getUid());
} else if (isOpCamera(item.getCode())) {
paused = mCameraDisabled;
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt
new file mode 100644
index 000000000000..a6fb1502ed19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import androidx.annotation.StringRes
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.res.R
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+
+sealed class AudioSharingButtonState {
+ object Gone : AudioSharingButtonState()
+
+ data class Visible(@StringRes val resId: Int, val isActive: Boolean) :
+ AudioSharingButtonState()
+}
+
+class AudioSharingButtonViewModel
+@AssistedInject
+constructor(
+ private val localBluetoothManager: LocalBluetoothManager?,
+ private val audioSharingInteractor: AudioSharingInteractor,
+ private val bluetoothStateInteractor: BluetoothStateInteractor,
+ private val deviceItemInteractor: DeviceItemInteractor,
+) : ExclusiveActivatable() {
+
+ private val mutableButtonState =
+ MutableStateFlow<AudioSharingButtonState>(AudioSharingButtonState.Gone)
+ /** Flow representing the update of AudioSharingButtonState. */
+ val audioSharingButtonStateUpdate: StateFlow<AudioSharingButtonState> =
+ mutableButtonState.asStateFlow()
+
+ override suspend fun onActivated(): Nothing {
+ combine(
+ bluetoothStateInteractor.bluetoothStateUpdate,
+ deviceItemInteractor.deviceItemUpdate,
+ audioSharingInteractor.isAudioSharingOn
+ ) { bluetoothState, deviceItem, audioSharingOn ->
+ getButtonState(bluetoothState, deviceItem, audioSharingOn)
+ }
+ .collect { mutableButtonState.value = it }
+ awaitCancellation()
+ }
+
+ private fun getButtonState(
+ bluetoothState: Boolean,
+ deviceItem: List<DeviceItem>,
+ audioSharingOn: Boolean
+ ): AudioSharingButtonState {
+ return when {
+ // Don't show button when bluetooth is off
+ !bluetoothState -> AudioSharingButtonState.Gone
+ // Show sharing audio when broadcasting
+ audioSharingOn ->
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
+ isActive = true
+ )
+ // When not broadcasting, don't show button if there's connected source in any device
+ deviceItem.any {
+ BluetoothUtils.hasConnectedBroadcastSource(
+ it.cachedBluetoothDevice,
+ localBluetoothManager
+ )
+ } -> AudioSharingButtonState.Gone
+ // Show audio sharing when there's a connected LE audio device
+ deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } ->
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button,
+ isActive = false
+ )
+ else -> AudioSharingButtonState.Gone
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): AudioSharingButtonViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
new file mode 100644
index 000000000000..692a78be075f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.A2dpProfile
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.HeadsetProfile
+import com.android.settingslib.bluetooth.HearingAidProfile
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class AudioSharingDeviceItemActionInteractorImpl
+@Inject
+constructor(
+ private val activityStarter: ActivityStarter,
+ private val audioSharingInteractor: AudioSharingInteractor,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val localBluetoothManager: LocalBluetoothManager?,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val logger: BluetoothTileDialogLogger,
+ private val uiEventLogger: UiEventLogger,
+ private val delegateFactory: AudioSharingDialogDelegate.Factory,
+ private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl,
+) : DeviceItemActionInteractor {
+
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ withContext(backgroundDispatcher) {
+ if (!audioSharingInteractor.audioSharingAvailable()) {
+ return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
+ }
+ val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager)
+ logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing)
+
+ when {
+ deviceItem.type ==
+ DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ if (audioSharingQsDialogImprovement()) {
+ withContext(mainDispatcher) {
+ delegateFactory
+ .create(deviceItem.cachedBluetoothDevice)
+ .createDialog()
+ .let { dialogTransitionAnimator.showFromDialog(it, dialog) }
+ }
+ } else {
+ launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
+ logger.logLaunchSettingsCriteriaMatched(
+ "AvailableAudioSharingDeviceClicked",
+ deviceItem,
+ )
+ }
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED
+ )
+ }
+ inSharingAndDeviceNoSource(inAudioSharing, deviceItem) -> {
+ launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
+ logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem)
+ uiEventLogger.log(
+ if (deviceItem.isLeAudioSupported)
+ BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED
+ else
+ BluetoothTileDialogUiEvent
+ .LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED
+ )
+ }
+ else -> {
+ deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
+ }
+ }
+ }
+ }
+
+ private fun inSharingAndDeviceNoSource(
+ inAudioSharing: Boolean,
+ deviceItem: DeviceItem,
+ ): Boolean {
+ return inAudioSharing &&
+ deviceItem.isMediaDevice &&
+ !BluetoothUtils.hasConnectedBroadcastSource(
+ deviceItem.cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ }
+
+ private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
+ val intent =
+ Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
+ putExtra(
+ EXTRA_SHOW_FRAGMENT_ARGUMENTS,
+ Bundle().apply {
+ putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device)
+ },
+ )
+ }
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
+ activityStarter.postStartActivityDismissingKeyguard(
+ intent,
+ 0,
+ dialogTransitionAnimator.createActivityTransitionController(dialog),
+ )
+ }
+
+ private companion object {
+ const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
+
+ val DeviceItem.isLeAudioSupported: Boolean
+ get() =
+ cachedBluetoothDevice.profiles.any { profile ->
+ profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device)
+ }
+
+ val DeviceItem.isMediaDevice: Boolean
+ get() =
+ cachedBluetoothDevice.uiAccessibleProfiles.any {
+ it is A2dpProfile ||
+ it is HearingAidProfile ||
+ it is LeAudioProfile ||
+ it is HeadsetProfile
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt
new file mode 100644
index 000000000000..3ac942b769f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.TextView
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class AudioSharingDialogDelegate
+@AssistedInject
+constructor(
+ @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice,
+ @Application private val coroutineScope: CoroutineScope,
+ private val viewModelFactory: AudioSharingDialogViewModel.Factory,
+ private val sysuiDialogFactory: SystemUIDialog.Factory,
+ private val uiEventLogger: UiEventLogger,
+) : SystemUIDialog.Delegate {
+
+ override fun createDialog(): SystemUIDialog = sysuiDialogFactory.create(this)
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ with(dialog.layoutInflater.inflate(R.layout.audio_sharing_dialog, null)) {
+ dialog.setView(this)
+ val subtitleTextView = requireViewById<TextView>(R.id.subtitle)
+ val shareAudioButton = requireViewById<TextView>(R.id.share_audio_button)
+ val switchActiveButton = requireViewById<Button>(R.id.switch_active_button)
+ val job =
+ coroutineScope.launch {
+ val viewModel = viewModelFactory.create(cachedBluetoothDevice, this)
+ viewModel.dialogState.collect {
+ when (it) {
+ is AudioSharingDialogState.Hide -> dialog.dismiss()
+ is AudioSharingDialogState.Show -> {
+ subtitleTextView.text = it.subtitle
+ switchActiveButton.text = it.switchButtonText
+ switchActiveButton.setOnClickListener {
+ viewModel.switchActiveClicked()
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent
+ .AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED
+ )
+ dialog.dismiss()
+ }
+ shareAudioButton.setOnClickListener {
+ viewModel.shareAudioClicked()
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent
+ .AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED
+ )
+ dialog.dismiss()
+ }
+ }
+ }
+ }
+ }
+ SystemUIDialog.registerDismissListener(dialog) { job.cancel() }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(cachedBluetoothDevice: CachedBluetoothDevice): AudioSharingDialogDelegate
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt
new file mode 100644
index 000000000000..dc970aea7c41
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.content.Context
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+sealed class AudioSharingDialogState {
+ data object Hide : AudioSharingDialogState()
+
+ data class Show(val subtitle: String, val switchButtonText: String) : AudioSharingDialogState()
+}
+
+class AudioSharingDialogViewModel
+@AssistedInject
+constructor(
+ deviceItemInteractor: DeviceItemInteractor,
+ private val audioSharingInteractor: AudioSharingInteractor,
+ private val context: Context,
+ private val localBluetoothManager: LocalBluetoothManager?,
+ @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice,
+ @Assisted private val coroutineScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ val dialogState: Flow<AudioSharingDialogState> =
+ deviceItemInteractor.deviceItemUpdateRequest
+ .map {
+ if (
+ audioSharingInteractor.isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice
+ )
+ ) {
+ createShowState(cachedBluetoothDevice)
+ } else {
+ AudioSharingDialogState.Hide
+ }
+ }
+ .onStart { emit(createShowState(cachedBluetoothDevice)) }
+ .flowOn(backgroundDispatcher)
+ .distinctUntilChanged()
+
+ fun switchActiveClicked() {
+ coroutineScope.launch { audioSharingInteractor.switchActive(cachedBluetoothDevice) }
+ }
+
+ fun shareAudioClicked() {
+ coroutineScope.launch { audioSharingInteractor.startAudioSharing() }
+ }
+
+ private fun createShowState(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): AudioSharingDialogState {
+ val activeDeviceName =
+ localBluetoothManager
+ ?.profileManager
+ ?.leAudioProfile
+ ?.activeDevices
+ ?.firstOrNull()
+ ?.let { localBluetoothManager.cachedDeviceManager?.findDevice(it)?.name } ?: ""
+ val availableDeviceName = cachedBluetoothDevice.name
+ return AudioSharingDialogState.Show(
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ availableDeviceName,
+ activeDeviceName
+ ),
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ availableDeviceName
+ )
+ )
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ cachedBluetoothDevice: CachedBluetoothDevice,
+ coroutineScope: CoroutineScope
+ ): AudioSharingDialogViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
index 817f2d7f6f70..65f110533573 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
@@ -16,82 +16,148 @@
package com.android.systemui.bluetooth.qsdialog
-import androidx.annotation.StringRes
import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onPlaybackStarted
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.withContext
-internal sealed class AudioSharingButtonState {
- object Gone : AudioSharingButtonState()
+/** Holds business logic for the audio sharing state. */
+interface AudioSharingInteractor {
+ val isAudioSharingOn: Flow<Boolean>
+
+ val audioSourceStateUpdate: Flow<Unit>
+
+ suspend fun handleAudioSourceWhenReady()
+
+ suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): Boolean
+
+ suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice)
- data class Visible(@StringRes val resId: Int, val isActive: Boolean) :
- AudioSharingButtonState()
+ suspend fun startAudioSharing()
+
+ suspend fun audioSharingAvailable(): Boolean
}
-/** Holds business logic for the audio sharing state. */
@SysUISingleton
-internal class AudioSharingInteractor
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingInteractorImpl
@Inject
constructor(
private val localBluetoothManager: LocalBluetoothManager?,
- bluetoothStateInteractor: BluetoothStateInteractor,
- deviceItemInteractor: DeviceItemInteractor,
- @Application private val coroutineScope: CoroutineScope,
+ private val audioSharingRepository: AudioSharingRepository,
@Background private val backgroundDispatcher: CoroutineDispatcher,
-) {
- /** Flow representing the update of AudioSharingButtonState. */
- internal val audioSharingButtonStateUpdate: Flow<AudioSharingButtonState> =
- combine(
- bluetoothStateInteractor.bluetoothStateUpdate,
- deviceItemInteractor.deviceItemUpdate
- ) { bluetoothState, deviceItem ->
- getButtonState(bluetoothState, deviceItem)
+) : AudioSharingInteractor {
+
+ override val isAudioSharingOn: Flow<Boolean> =
+ flow { emit(audioSharingAvailable()) }
+ .flatMapLatest { isEnabled ->
+ if (isEnabled) {
+ audioSharingRepository.inAudioSharing
+ } else {
+ flowOf(false)
+ }
}
.flowOn(backgroundDispatcher)
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
- initialValue = AudioSharingButtonState.Gone
- )
-
- private fun getButtonState(
- bluetoothState: Boolean,
- deviceItem: List<DeviceItem>
- ): AudioSharingButtonState {
- return when {
- // Don't show button when bluetooth is off
- !bluetoothState -> AudioSharingButtonState.Gone
- // Show sharing audio when broadcasting
- BluetoothUtils.isBroadcasting(localBluetoothManager) ->
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
- isActive = true
- )
- // When not broadcasting, don't show button if there's connected source in any device
- deviceItem.any {
- BluetoothUtils.hasConnectedBroadcastSource(
- it.cachedBluetoothDevice,
- localBluetoothManager
- )
- } -> AudioSharingButtonState.Gone
- // Show audio sharing when there's a connected LE audio device
- deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } ->
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button,
- isActive = false
+
+ override val audioSourceStateUpdate =
+ isAudioSharingOn
+ .flatMapLatest {
+ if (it) {
+ audioSharingRepository.audioSourceStateUpdate
+ } else {
+ emptyFlow()
+ }
+ }
+ .flowOn(backgroundDispatcher)
+
+ override suspend fun handleAudioSourceWhenReady() {
+ withContext(backgroundDispatcher) {
+ if (audioSharingAvailable()) {
+ audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
+ isAudioSharingOn
+ .mapNotNull { audioSharingOn ->
+ if (audioSharingOn) {
+ // onPlaybackStarted could emit multiple times during one
+ // audio sharing session, we only perform add source on the
+ // first time
+ profile.onPlaybackStarted.firstOrNull()
+ } else {
+ null
+ }
+ }
+ .flowOn(backgroundDispatcher)
+ .collect { audioSharingRepository.addSource() }
+ }
+ }
+ }
+ }
+
+ override suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): Boolean {
+ return withContext(backgroundDispatcher) {
+ if (audioSharingAvailable()) {
+ BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice,
+ localBluetoothManager,
)
- else -> AudioSharingButtonState.Gone
+ } else {
+ false
+ }
}
}
+
+ override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {
+ if (!audioSharingAvailable()) {
+ return
+ }
+ audioSharingRepository.setActive(cachedBluetoothDevice)
+ }
+
+ override suspend fun startAudioSharing() {
+ if (!audioSharingAvailable()) {
+ return
+ }
+ audioSharingRepository.startAudioSharing()
+ }
+
+ // TODO(b/367965193): Move this after flags rollout
+ override suspend fun audioSharingAvailable(): Boolean {
+ return audioSharingRepository.audioSharingAvailable()
+ }
+}
+
+@SysUISingleton
+class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor {
+ override val isAudioSharingOn: Flow<Boolean> = flowOf(false)
+
+ override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+
+ override suspend fun handleAudioSourceWhenReady() {}
+
+ override suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ) = false
+
+ override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+ override suspend fun startAudioSharing() {}
+
+ override suspend fun audioSharingAvailable(): Boolean = false
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
new file mode 100644
index 000000000000..b9b8d36d41e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
+import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.withContext
+
+interface AudioSharingRepository {
+ val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+
+ val audioSourceStateUpdate: Flow<Unit>
+
+ val inAudioSharing: StateFlow<Boolean>
+
+ suspend fun audioSharingAvailable(): Boolean
+
+ suspend fun addSource()
+
+ suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice)
+
+ suspend fun startAudioSharing()
+}
+
+@SysUISingleton
+class AudioSharingRepositoryImpl(
+ private val localBluetoothManager: LocalBluetoothManager,
+ private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : AudioSharingRepository {
+
+ override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+ get() = localBluetoothManager.profileManager?.leAudioBroadcastProfile
+
+ private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant?
+ get() = localBluetoothManager.profileManager?.leAudioBroadcastAssistantProfile
+
+ override val audioSourceStateUpdate: Flow<Unit> =
+ leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow()
+
+ override val inAudioSharing: StateFlow<Boolean> =
+ settingsLibAudioSharingRepository.inAudioSharing
+
+ override suspend fun audioSharingAvailable(): Boolean {
+ return settingsLibAudioSharingRepository.audioSharingAvailable()
+ }
+
+ override suspend fun addSource() {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ leAudioBroadcastProfile?.latestBluetoothLeBroadcastMetadata?.let { metadata ->
+ leAudioBroadcastAssistantProfile?.let {
+ it.allConnectedDevices.forEach { sink -> it.addSource(sink, metadata, false) }
+ }
+ }
+ }
+ }
+
+ override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ cachedBluetoothDevice.setActive()
+ }
+ }
+
+ override suspend fun startAudioSharing() {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ leAudioBroadcastProfile?.startPrivateBroadcast()
+ }
+ }
+}
+
+@SysUISingleton
+class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
+ override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null
+
+ override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+
+ override val inAudioSharing: StateFlow<Boolean> = MutableStateFlow(false)
+
+ override suspend fun audioSharingAvailable(): Boolean = false
+
+ override suspend fun addSource() {}
+
+ override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+ override suspend fun startAudioSharing() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
index 17f9e634ec62..55d4d3efbe27 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
@@ -39,7 +39,7 @@ import kotlinx.coroutines.withContext
/** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */
@SysUISingleton
-internal class BluetoothStateInteractor
+class BluetoothStateInteractor
@Inject
constructor(
private val localBluetoothManager: LocalBluetoothManager?,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index 7deea7335223..a9c5c69ca26e 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -300,7 +300,7 @@ internal constructor(
}
private fun getProgressBarBackground(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
+ return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
}
private fun getScrollViewContent(dialog: SystemUIDialog): View {
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
index bdd4c161ad59..aad233fe40ca 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
@@ -42,6 +42,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent
LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED(1717),
@UiEvent(doc = "Currently broadcasting and a non-LE audio supported device is clicked")
LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED(1718),
+ @Deprecated("Use case no longer needed")
@UiEvent(
doc = "Not broadcasting, having one connected, another saved LE audio device is clicked"
)
@@ -52,8 +53,13 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent
)
@UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked")
LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720),
+ @Deprecated("Use case no longer needed")
@UiEvent(doc = "Not broadcasting, having two connected, the active LE audio devices is clicked")
- LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881);
+ LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881),
+ @UiEvent(doc = "Clicked on switch active button on audio sharing dialog")
+ AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890),
+ @UiEvent(doc = "Clicked on share audio button on audio sharing dialog")
+ AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891);
override fun getId() = metricId
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index a8f7fc345001..5c35c52a4327 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -28,8 +28,8 @@ import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
import com.android.systemui.Prefs
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
@@ -51,6 +51,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge
@@ -68,10 +69,12 @@ constructor(
private val bluetoothStateInteractor: BluetoothStateInteractor,
private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
private val audioSharingInteractor: AudioSharingInteractor,
+ private val audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory,
private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor,
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val activityStarter: ActivityStarter,
private val uiEventLogger: UiEventLogger,
+ private val logger: BluetoothTileDialogLogger,
@Application private val coroutineScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundDispatcher: CoroutineDispatcher,
@@ -102,7 +105,7 @@ constructor(
expandable?.dialogTransitionController(
DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
+ INTERACTION_JANK_TAG,
)
)
controller?.let {
@@ -117,7 +120,7 @@ constructor(
// stop the progress bar.
combine(
deviceItemInteractor.deviceItemUpdate,
- deviceItemInteractor.showSeeAllUpdate
+ deviceItemInteractor.showSeeAllUpdate,
) { deviceItem, showSeeAll ->
updateDialogUiJob?.cancel()
updateDialogUiJob = launch {
@@ -127,7 +130,7 @@ constructor(
deviceItem,
showSeeAll,
showPairNewDevice =
- bluetoothStateInteractor.isBluetoothEnabled()
+ bluetoothStateInteractor.isBluetoothEnabled(),
)
animateProgressBar(dialog, false)
}
@@ -139,7 +142,15 @@ constructor(
// the device item list and animate the progress bar.
merge(
deviceItemInteractor.deviceItemUpdateRequest,
- bluetoothDeviceMetadataInteractor.metadataUpdate
+ bluetoothDeviceMetadataInteractor.metadataUpdate,
+ if (
+ audioSharingInteractor.audioSharingAvailable() &&
+ audioSharingQsDialogImprovement()
+ ) {
+ audioSharingInteractor.audioSourceStateUpdate
+ } else {
+ emptyFlow()
+ },
)
.onEach {
dialogDelegate.animateProgressBar(dialog, true)
@@ -147,35 +158,42 @@ constructor(
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(
context,
- DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED
+ DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED,
)
}
}
.launchIn(this)
- if (BluetoothUtils.isAudioSharingEnabled()) {
- audioSharingInteractor.audioSharingButtonStateUpdate
- .onEach {
- when (it) {
- is AudioSharingButtonState.Visible -> {
- dialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- VISIBLE,
- context.getString(it.resId),
- it.isActive
- )
- }
- is AudioSharingButtonState.Gone -> {
- dialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- GONE,
- label = null,
- isActive = false
- )
+ if (audioSharingInteractor.audioSharingAvailable()) {
+ if (audioSharingQsDialogImprovement()) {
+ launch { audioSharingInteractor.handleAudioSourceWhenReady() }
+ }
+
+ audioSharingButtonViewModelFactory.create().run {
+ audioSharingButtonStateUpdate
+ .onEach {
+ when (it) {
+ is AudioSharingButtonState.Visible -> {
+ dialogDelegate.onAudioSharingButtonUpdated(
+ dialog,
+ VISIBLE,
+ context.getString(it.resId),
+ it.isActive,
+ )
+ }
+ is AudioSharingButtonState.Gone -> {
+ dialogDelegate.onAudioSharingButtonUpdated(
+ dialog,
+ GONE,
+ label = null,
+ isActive = false,
+ )
+ }
}
}
- }
- .launchIn(this)
+ .launchIn(this@launch)
+ launch { activate() }
+ }
}
// bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch
@@ -185,13 +203,13 @@ constructor(
dialogDelegate.onBluetoothStateUpdated(
dialog,
it,
- UiProperties.build(it, isAutoOnToggleFeatureAvailable())
+ UiProperties.build(it, isAutoOnToggleFeatureAvailable()),
)
updateDeviceItemJob?.cancel()
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(
context,
- DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED
+ DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED,
)
}
}
@@ -209,7 +227,10 @@ constructor(
// deviceItemClick is emitted when user clicked on a device item.
dialogDelegate.deviceItemClick
- .onEach { deviceItemActionInteractor.onClick(it, dialog) }
+ .onEach {
+ deviceItemActionInteractor.onClick(it, dialog)
+ logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type)
+ }
.launchIn(this)
// contentHeight is emitted when the dialog is dismissed.
@@ -230,7 +251,7 @@ constructor(
dialog,
it,
if (it) R.string.turn_on_bluetooth_auto_info_enabled
- else R.string.turn_on_bluetooth_auto_info_disabled
+ else R.string.turn_on_bluetooth_auto_info_disabled,
)
}
.launchIn(this)
@@ -252,18 +273,18 @@ constructor(
withContext(backgroundDispatcher) {
sharedPreferences.getInt(
CONTENT_HEIGHT_PREF_KEY,
- ViewGroup.LayoutParams.WRAP_CONTENT
+ ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
return bluetoothDialogDelegateFactory.create(
UiProperties.build(
bluetoothStateInteractor.isBluetoothEnabled(),
- isAutoOnToggleFeatureAvailable()
+ isAutoOnToggleFeatureAvailable(),
),
cachedContentHeight,
this@BluetoothTileDialogViewModel,
- { cancelJob() }
+ { cancelJob() },
)
}
@@ -275,7 +296,7 @@ constructor(
EXTRA_SHOW_FRAGMENT_ARGUMENTS,
Bundle().apply {
putString("device_address", deviceItem.cachedBluetoothDevice.address)
- }
+ },
)
}
startSettingsActivity(intent, view)
@@ -299,7 +320,7 @@ constructor(
EXTRA_SHOW_FRAGMENT_ARGUMENTS,
Bundle().apply {
putBoolean(LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING, true)
- }
+ },
)
}
startSettingsActivity(intent, view)
@@ -345,7 +366,7 @@ constructor(
companion object {
internal fun build(
isBluetoothEnabled: Boolean,
- isAutoOnToggleFeatureAvailable: Boolean
+ isAutoOnToggleFeatureAvailable: Boolean,
) =
UiProperties(
subTitleResId = getSubtitleResId(isBluetoothEnabled),
@@ -355,7 +376,7 @@ constructor(
scrollViewMinHeightResId =
if (isAutoOnToggleFeatureAvailable)
R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
- else R.dimen.bluetooth_dialog_scroll_view_min_height
+ else R.dimen.bluetooth_dialog_scroll_view_min_height,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index f1894d3bb111..cf0f19f1d361 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -16,87 +16,28 @@
package com.android.systemui.bluetooth.qsdialog
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothProfile
-import android.content.Intent
-import android.os.Bundle
-import android.provider.Settings
import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.A2dpProfile
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.HeadsetProfile
-import com.android.settingslib.bluetooth.HearingAidProfile
-import com.android.settingslib.bluetooth.LeAudioProfile
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor.LaunchSettingsCriteria.Companion.getCurrentConnectedLeByGroupId
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
+interface DeviceItemActionInteractor {
+ suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {}
+}
+
@SysUISingleton
-class DeviceItemActionInteractor
+class DeviceItemActionInteractorImpl
@Inject
constructor(
- private val activityStarter: ActivityStarter,
- private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val localBluetoothManager: LocalBluetoothManager?,
@Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
private val uiEventLogger: UiEventLogger,
-) {
- private val leAudioProfile: LeAudioProfile?
- get() = localBluetoothManager?.profileManager?.leAudioProfile
-
- private val assistantProfile: LocalBluetoothLeBroadcastAssistant?
- get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile
-
- private val launchSettingsCriteriaList: List<LaunchSettingsCriteria>
- get() =
- listOf(
- InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger),
- NotSharingClickedNonConnect(
- leAudioProfile,
- assistantProfile,
- backgroundDispatcher,
- logger
- ),
- NotSharingClickedActive(
- leAudioProfile,
- assistantProfile,
- backgroundDispatcher,
- logger
- )
- )
+) : DeviceItemActionInteractor {
- suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
withContext(backgroundDispatcher) {
- logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
- if (
- BluetoothUtils.isAudioSharingEnabled() &&
- localBluetoothManager != null &&
- leAudioProfile != null &&
- assistantProfile != null
- ) {
- val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager)
- logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing)
-
- val criteriaMatched =
- launchSettingsCriteriaList.firstOrNull {
- it.matched(inAudioSharing, deviceItem)
- }
- if (criteriaMatched != null) {
- uiEventLogger.log(criteriaMatched.getClickUiEvent(deviceItem))
- launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
- return@withContext
- }
- }
deviceItem.cachedBluetoothDevice.apply {
when (deviceItem.type) {
DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
@@ -106,12 +47,6 @@ constructor(
DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
}
- DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
- // TODO(b/360759048): pop up dialog
- uiEventLogger.log(
- BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED
- )
- }
DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
setActive()
uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
@@ -126,186 +61,12 @@ constructor(
connect()
uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
}
- }
- }
- }
- }
-
- private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
- val intent =
- Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
- putExtra(
- EXTRA_SHOW_FRAGMENT_ARGUMENTS,
- Bundle().apply {
- putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device)
+ DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ // Do nothing. Should already be handled in
+ // AudioSharingDeviceItemActionInteractor.
}
- )
- }
- intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
- activityStarter.postStartActivityDismissingKeyguard(
- intent,
- 0,
- dialogTransitionAnimator.createActivityTransitionController(dialog)
- )
- }
-
- private interface LaunchSettingsCriteria {
- suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean
-
- suspend fun getClickUiEvent(deviceItem: DeviceItem): BluetoothTileDialogUiEvent
-
- companion object {
- suspend fun getCurrentConnectedLeByGroupId(
- leAudioProfile: LeAudioProfile,
- assistantProfile: LocalBluetoothLeBroadcastAssistant,
- @Background backgroundDispatcher: CoroutineDispatcher,
- logger: BluetoothTileDialogLogger,
- ): Map<Int, List<BluetoothDevice>> {
- return withContext(backgroundDispatcher) {
- assistantProfile
- .getDevicesMatchingConnectionStates(
- intArrayOf(BluetoothProfile.STATE_CONNECTED)
- )
- ?.filterNotNull()
- ?.groupBy { leAudioProfile.getGroupId(it) }
- ?.also { logger.logConnectedLeByGroupId(it) } ?: emptyMap()
}
}
}
}
-
- private class InSharingClickedNoSource(
- private val localBluetoothManager: LocalBluetoothManager?,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
- ) : LaunchSettingsCriteria {
- // If currently broadcasting and the clicked device is not connected to the source
- override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
- return withContext(backgroundDispatcher) {
- val matched =
- inAudioSharing &&
- deviceItem.isMediaDevice &&
- !BluetoothUtils.hasConnectedBroadcastSource(
- deviceItem.cachedBluetoothDevice,
- localBluetoothManager
- )
-
- if (matched) {
- logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem)
- }
-
- matched
- }
- }
-
- override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
- if (deviceItem.isLeAudioSupported)
- BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED
- else BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED
- }
-
- private class NotSharingClickedNonConnect(
- private val leAudioProfile: LeAudioProfile?,
- private val assistantProfile: LocalBluetoothLeBroadcastAssistant?,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
- ) : LaunchSettingsCriteria {
- // If not broadcasting, having one device connected, and clicked on a not yet connected LE
- // audio device
- override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
- return withContext(backgroundDispatcher) {
- val matched =
- leAudioProfile?.let { leAudio ->
- assistantProfile?.let { assistant ->
- !inAudioSharing &&
- getCurrentConnectedLeByGroupId(
- leAudio,
- assistant,
- backgroundDispatcher,
- logger
- )
- .size == 1 &&
- deviceItem.isNotConnectedLeAudioSupported
- }
- } ?: false
-
- if (matched) {
- logger.logLaunchSettingsCriteriaMatched(
- "NotSharingClickedNonConnect",
- deviceItem
- )
- }
-
- matched
- }
- }
-
- override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
- BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED
- }
-
- private class NotSharingClickedActive(
- private val leAudioProfile: LeAudioProfile?,
- private val assistantProfile: LocalBluetoothLeBroadcastAssistant?,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
- ) : LaunchSettingsCriteria {
- // If not broadcasting, having two device connected, clicked on the active LE audio
- // device
- override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
- return withContext(backgroundDispatcher) {
- val matched =
- leAudioProfile?.let { leAudio ->
- assistantProfile?.let { assistant ->
- !inAudioSharing &&
- getCurrentConnectedLeByGroupId(
- leAudio,
- assistant,
- backgroundDispatcher,
- logger
- )
- .size == 2 &&
- deviceItem.isActiveLeAudioSupported
- }
- } ?: false
-
- if (matched) {
- logger.logLaunchSettingsCriteriaMatched(
- "NotSharingClickedConnected",
- deviceItem
- )
- }
-
- matched
- }
- }
-
- override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
- BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED
- }
-
- private companion object {
- const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
-
- val DeviceItem.isLeAudioSupported: Boolean
- get() =
- cachedBluetoothDevice.profiles.any { profile ->
- profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device)
- }
-
- val DeviceItem.isNotConnectedLeAudioSupported: Boolean
- get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported
-
- val DeviceItem.isActiveLeAudioSupported: Boolean
- get() = type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE && isLeAudioSupported
-
- val DeviceItem.isMediaDevice: Boolean
- get() =
- cachedBluetoothDevice.uiAccessibleProfiles.any {
- it is A2dpProfile ||
- it is HearingAidProfile ||
- it is LeAudioProfile ||
- it is HeadsetProfile
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 7280489e0835..7ed56296e865 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -23,7 +23,6 @@ import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.flags.Flags
-import com.android.settingslib.flags.Flags.enableLeAudioSharing
import com.android.systemui.res.R
private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on
@@ -56,7 +55,7 @@ abstract class DeviceItemFactory {
connectionSummary: String,
background: Int,
actionAccessibilityLabel: String,
- isActive: Boolean
+ isActive: Boolean,
): DeviceItem {
return DeviceItem(
type = type,
@@ -70,7 +69,7 @@ abstract class DeviceItemFactory {
background = background,
isEnabled = !cachedDevice.isBusy,
actionAccessibilityLabel = actionAccessibilityLabel,
- isActive = isActive
+ isActive = isActive,
)
}
}
@@ -80,7 +79,7 @@ internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
@@ -94,20 +93,20 @@ internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() {
cachedDevice.connectionSummary ?: "",
backgroundOn,
context.getString(actionAccessibilityLabelDisconnect),
- isActive = true
+ isActive = true,
)
}
}
internal class AudioSharingMediaDeviceItemFactory(
- private val localBluetoothManager: LocalBluetoothManager?
+ private val localBluetoothManager: LocalBluetoothManager
) : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
- return enableLeAudioSharing() &&
+ return BluetoothUtils.isAudioSharingEnabled() &&
BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager)
}
@@ -120,24 +119,24 @@ internal class AudioSharingMediaDeviceItemFactory(
?: context.getString(audioSharing),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
"",
- isActive = !cachedDevice.isBusy
+ isActive = !cachedDevice.isBusy,
)
}
}
internal class AvailableAudioSharingMediaDeviceItemFactory(
- private val localBluetoothManager: LocalBluetoothManager?
+ private val localBluetoothManager: LocalBluetoothManager
) : AvailableMediaDeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return BluetoothUtils.isAudioSharingEnabled() &&
super.isFilterMatched(context, cachedDevice, audioManager) &&
BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
cachedDevice,
- localBluetoothManager
+ localBluetoothManager,
)
}
@@ -151,7 +150,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory(
),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
"",
- isActive = false
+ isActive = false,
)
}
}
@@ -160,7 +159,7 @@ internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableHearingDevice(cachedDevice)
@@ -171,7 +170,7 @@ open class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
@@ -186,7 +185,7 @@ open class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
?: context.getString(connected),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
context.getString(actionAccessibilityLabelActivate),
- isActive = false
+ isActive = false,
)
}
}
@@ -195,7 +194,7 @@ internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFacto
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableHearingDevice(cachedDevice)
@@ -206,7 +205,7 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
!BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
@@ -225,7 +224,7 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
?: context.getString(connected),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
context.getString(actionAccessibilityLabelDisconnect),
- isActive = false
+ isActive = false,
)
}
}
@@ -234,7 +233,7 @@ internal open class SavedDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
!BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
@@ -254,7 +253,7 @@ internal open class SavedDeviceItemFactory : DeviceItemFactory() {
?: context.getString(saved),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
context.getString(actionAccessibilityLabelActivate),
- isActive = false
+ isActive = false,
)
}
}
@@ -263,12 +262,12 @@ internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
!BluetoothUtils.isExclusivelyManagedBluetoothDevice(
context,
- cachedDevice.getDevice()
+ cachedDevice.getDevice(),
) &&
cachedDevice.isHearingAidDevice &&
cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 9114ecac7ac7..0118e56a773c 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -54,6 +54,8 @@ constructor(
private val localBluetoothManager: LocalBluetoothManager?,
private val systemClock: SystemClock,
private val logger: BluetoothTileDialogLogger,
+ private val deviceItemFactoryList: List<@JvmSuppressWildcards DeviceItemFactory>,
+ private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>,
@Application private val coroutineScope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) {
@@ -67,7 +69,7 @@ constructor(
internal val showSeeAllUpdate
get() = mutableShowSeeAllUpdate.asStateFlow()
- internal val deviceItemUpdateRequest: SharedFlow<Unit> =
+ val deviceItemUpdateRequest: SharedFlow<Unit> =
conflatedCallbackFlow {
val listener =
object : BluetoothCallback {
@@ -114,26 +116,6 @@ constructor(
}
.shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
- private var deviceItemFactoryList: List<DeviceItemFactory> =
- listOf(
- ActiveMediaDeviceItemFactory(),
- AudioSharingMediaDeviceItemFactory(localBluetoothManager),
- AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager),
- AvailableMediaDeviceItemFactory(),
- ConnectedDeviceItemFactory(),
- SavedDeviceItemFactory()
- )
-
- private var displayPriority: List<DeviceItemType> =
- listOf(
- DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
- DeviceItemType.SAVED_BLUETOOTH_DEVICE,
- )
-
internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) {
withContext(backgroundDispatcher) {
val start = systemClock.elapsedRealtime()
@@ -144,7 +126,7 @@ constructor(
.firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) }
?.create(context, cachedDevice)
}
- .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
+ .sort(deviceItemDisplayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
// Only emit when the job is not cancelled
if (isActive) {
mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY))
@@ -176,14 +158,6 @@ constructor(
)
}
- internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
- deviceItemFactoryList = list
- }
-
- internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) {
- displayPriority = list
- }
-
companion object {
private const val TAG = "DeviceItemInteractor"
private const val MAX_DEVICE_ITEM_ENTRY = 3
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
new file mode 100644
index 000000000000..50970a5d006f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog.dagger
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags
+import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
+import com.android.systemui.bluetooth.qsdialog.ActiveMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AudioSharingDeviceItemActionInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractor
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorEmptyImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepository
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryEmptyImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryImpl
+import com.android.systemui.bluetooth.qsdialog.AvailableAudioSharingMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AvailableMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor
+import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.DeviceItemType
+import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Dagger module for audio sharing code for BT QS dialog */
+@Module
+interface AudioSharingModule {
+
+ companion object {
+ @Provides
+ @SysUISingleton
+ fun provideAudioSharingRepository(
+ localBluetoothManager: LocalBluetoothManager?,
+ settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ ): AudioSharingRepository =
+ if (
+ Flags.enableLeAudioSharing() &&
+ Flags.audioSharingQsDialogImprovement() &&
+ localBluetoothManager != null
+ ) {
+ AudioSharingRepositoryImpl(
+ localBluetoothManager,
+ settingsLibAudioSharingRepository,
+ backgroundDispatcher,
+ )
+ } else {
+ AudioSharingRepositoryEmptyImpl()
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideAudioSharingInteractor(
+ localBluetoothManager: LocalBluetoothManager?,
+ impl: Lazy<AudioSharingInteractorImpl>,
+ emptyImpl: Lazy<AudioSharingInteractorEmptyImpl>,
+ ): AudioSharingInteractor =
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ impl.get()
+ } else {
+ emptyImpl.get()
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideDeviceItemActionInteractor(
+ localBluetoothManager: LocalBluetoothManager?,
+ audioSharingImpl: Lazy<AudioSharingDeviceItemActionInteractorImpl>,
+ impl: Lazy<DeviceItemActionInteractorImpl>,
+ ): DeviceItemActionInteractor =
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ audioSharingImpl.get()
+ } else {
+ impl.get()
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideDeviceItemFactoryList(
+ localBluetoothManager: LocalBluetoothManager?
+ ): List<DeviceItemFactory> = buildList {
+ add(ActiveMediaDeviceItemFactory())
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ add(AudioSharingMediaDeviceItemFactory(localBluetoothManager))
+ add(AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager))
+ }
+ add(AvailableMediaDeviceItemFactory())
+ add(ConnectedDeviceItemFactory())
+ add(SavedDeviceItemFactory())
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideDeviceItemDisplayPriority(
+ localBluetoothManager: LocalBluetoothManager?
+ ): List<DeviceItemType> = buildList {
+ add(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ add(DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
+ add(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
+ }
+ add(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+ add(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ add(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index 373671d01395..0949ea4d7797 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bouncer.domain.interactor
+import android.util.Log
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
@@ -46,6 +47,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/** Encapsulates business logic for interacting with the lock-screen alternate bouncer. */
@@ -137,6 +139,8 @@ constructor(
flowOf(false)
}
}
+ .distinctUntilChanged()
+ .onEach { Log.d(TAG, "canShowAlternateBouncer changed to $it") }
.stateIn(
scope = scope,
started = WhileSubscribed(),
@@ -234,5 +238,7 @@ constructor(
companion object {
private const val MIN_VISIBILITY_DURATION_UNTIL_TOUCHES_DISMISS_ALTERNATE_BOUNCER_MS = 200L
+
+ private const val TAG = "AlternateBouncerInteractor"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 3ae9250b2938..6508e4b574a3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -27,7 +27,6 @@ import android.os.UserHandle
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
-import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE
import com.android.systemui.communal.data.model.CommunalWidgetCategories
@@ -184,10 +183,10 @@ constructor(
val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
- /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
+ /** Launch the widget picker activity using the given startActivity method. */
suspend fun onOpenWidgetPicker(
resources: Resources,
- activityLauncher: ActivityResultLauncher<Intent>,
+ startActivity: (intent: Intent) -> Unit,
): Boolean =
withContext(backgroundDispatcher) {
val widgets = communalInteractor.widgetContent.first()
@@ -199,7 +198,7 @@ constructor(
}
getWidgetPickerActivityIntent(resources, excludeList)?.let {
try {
- activityLauncher.launch(it)
+ startActivity(it)
return@withContext true
} catch (e: Exception) {
Log.e(TAG, "Failed to launch widget picker activity", e)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 13b4aa9b55cb..8c14d63c0e84 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -27,14 +27,12 @@ import android.view.IWindowManager
import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
-import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.theme.PlatformTheme
import com.android.internal.logging.UiEventLogger
import com.android.systemui.Flags.communalEditWidgetsActivityFinishFix
@@ -51,6 +49,7 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.settings.UserTracker
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -64,12 +63,15 @@ constructor(
private val uiEventLogger: UiEventLogger,
private val widgetConfiguratorFactory: WidgetConfigurationController.Factory,
private val widgetSection: CommunalAppWidgetSection,
+ private val userTracker: UserTracker,
@CommunalLog logBuffer: LogBuffer,
) : ComponentActivity() {
companion object {
private const val TAG = "EditWidgetsActivity"
private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start"
+
+ private const val REQUEST_CODE_WIDGET_PICKER = 200
}
/**
@@ -110,7 +112,7 @@ constructor(
object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
- savedInstanceState: Bundle?
+ savedInstanceState: Bundle?,
) {
waitingForResult =
savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT)
@@ -172,41 +174,6 @@ constructor(
if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this)
else NopActivityController()
- private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
- registerForActivityResult(StartActivityForResult()) { result ->
- when (result.resultCode) {
- RESULT_OK -> {
- uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
-
- result.data?.let { intent ->
- val isPendingWidgetDrag =
- intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
- // Nothing to do when a widget is being dragged & dropped. The drop
- // target in the communal grid will receive the widget to be added (if
- // the user drops it over).
- if (!isPendingWidgetDrag) {
- val (componentName, user) = getWidgetExtraFromIntent(intent)
- if (componentName != null && user != null) {
- // Add widget at the end.
- communalViewModel.onAddWidget(
- componentName,
- user,
- configurator = widgetConfigurator,
- )
- } else {
- run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
- }
- }
- } ?: run { Log.w(TAG, "No data in result.") }
- }
- else ->
- Log.w(
- TAG,
- "Failed to receive result from widget picker, code=${result.resultCode}"
- )
- }
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -226,8 +193,7 @@ constructor(
PlatformTheme {
Box(
modifier =
- Modifier.fillMaxSize()
- .background(LocalAndroidColorScheme.current.surfaceDim),
+ Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceDim)
) {
CommunalHub(
viewModel = communalViewModel,
@@ -274,7 +240,13 @@ constructor(
private fun onOpenWidgetPicker() {
lifecycleScope.launch {
- communalViewModel.onOpenWidgetPicker(resources, addWidgetActivityLauncher)
+ communalViewModel.onOpenWidgetPicker(resources) { intent: Intent ->
+ startActivityForResultAsUser(
+ intent,
+ REQUEST_CODE_WIDGET_PICKER,
+ userTracker.userHandle,
+ )
+ }
}
}
@@ -285,7 +257,7 @@ constructor(
communalViewModel.changeScene(
scene = CommunalScenes.Communal,
loggingReason = "edit mode closing",
- transitionKey = CommunalTransitionKeys.FromEditMode
+ transitionKey = CommunalTransitionKeys.FromEditMode,
)
// Wait for the current scene to be idle on communal.
@@ -309,7 +281,7 @@ constructor(
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
- options: Bundle?
+ options: Bundle?,
) {
activityController.onWaitingForResult(true)
super.startIntentSenderForResult(
@@ -319,15 +291,46 @@ constructor(
flagsMask,
flagsValues,
extraFlags,
- options
+ options,
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
activityController.onWaitingForResult(false)
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == WidgetConfigurationController.REQUEST_CODE) {
- widgetConfigurator.setConfigurationResult(resultCode)
+
+ when (requestCode) {
+ WidgetConfigurationController.REQUEST_CODE ->
+ widgetConfigurator.setConfigurationResult(resultCode)
+ REQUEST_CODE_WIDGET_PICKER -> {
+ if (resultCode != RESULT_OK) {
+ Log.w(TAG, "Failed to receive result from widget picker, code=$resultCode")
+ return
+ }
+
+ uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
+
+ data?.let { intent ->
+ val isPendingWidgetDrag =
+ intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
+ // Nothing to do when a widget is being dragged & dropped. The drop
+ // target in the communal grid will receive the widget to be added (if
+ // the user drops it over).
+ if (!isPendingWidgetDrag) {
+ val (componentName, user) = getWidgetExtraFromIntent(intent)
+ if (componentName != null && user != null) {
+ // Add widget at the end.
+ communalViewModel.onAddWidget(
+ componentName,
+ user,
+ configurator = widgetConfigurator,
+ )
+ } else {
+ run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
+ }
+ }
+ } ?: run { Log.w(TAG, "No data in result.") }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
index b56ed8c4c090..589dbf92de38 100644
--- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
@@ -24,6 +24,8 @@ import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayRepositoryImpl
import com.android.systemui.display.data.repository.DisplayScopeRepository
import com.android.systemui.display.data.repository.DisplayScopeRepositoryImpl
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl
import com.android.systemui.display.data.repository.FocusedDisplayRepository
import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -58,6 +60,11 @@ interface DisplayModule {
@Binds fun displayScopeRepository(impl: DisplayScopeRepositoryImpl): DisplayScopeRepository
+ @Binds
+ fun displayWindowPropertiesRepository(
+ impl: DisplayWindowPropertiesRepositoryImpl
+ ): DisplayWindowPropertiesRepository
+
companion object {
@Provides
@SysUISingleton
@@ -72,5 +79,19 @@ interface DisplayModule {
CoreStartable.NOP
}
}
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(DisplayWindowPropertiesRepository::class)
+ fun displayWindowPropertiesRepoAsCoreStartable(
+ repoLazy: Lazy<DisplayWindowPropertiesRepositoryImpl>
+ ): CoreStartable {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ return repoLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
new file mode 100644
index 000000000000..88d3a28669df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.Display
+import android.view.WindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.google.common.collect.HashBasedTable
+import com.google.common.collect.Table
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Provides per display instances of [DisplayWindowProperties]. */
+interface DisplayWindowPropertiesRepository {
+
+ /**
+ * Returns a [DisplayWindowProperties] instance for a given display id and window type.
+ *
+ * @throws IllegalArgumentException if no display with the given display id exists.
+ */
+ fun get(
+ displayId: Int,
+ @WindowManager.LayoutParams.WindowType windowType: Int,
+ ): DisplayWindowProperties
+}
+
+@SysUISingleton
+class DisplayWindowPropertiesRepositoryImpl
+@Inject
+constructor(
+ @Background private val backgroundApplicationScope: CoroutineScope,
+ private val globalContext: Context,
+ private val globalWindowManager: WindowManager,
+ private val displayRepository: DisplayRepository,
+) : DisplayWindowPropertiesRepository, CoreStartable {
+
+ init {
+ StatusBarConnectedDisplays.assertInNewMode()
+ }
+
+ private val properties: Table<Int, Int, DisplayWindowProperties> = HashBasedTable.create()
+
+ override fun get(
+ displayId: Int,
+ @WindowManager.LayoutParams.WindowType windowType: Int,
+ ): DisplayWindowProperties {
+ val display =
+ displayRepository.getDisplay(displayId)
+ ?: throw IllegalArgumentException("Display with id $displayId doesn't exist")
+ return properties.get(displayId, windowType)
+ ?: create(display, windowType).also { properties.put(displayId, windowType, it) }
+ }
+
+ override fun start() {
+ backgroundApplicationScope.launch(
+ CoroutineName("DisplayWindowPropertiesRepositoryImpl#start")
+ ) {
+ displayRepository.displayRemovalEvent.collect { removedDisplayId ->
+ properties.row(removedDisplayId).clear()
+ }
+ }
+ }
+
+ private fun create(display: Display, windowType: Int): DisplayWindowProperties {
+ val displayId = display.displayId
+ return if (displayId == Display.DEFAULT_DISPLAY) {
+ // For the default display, we can just reuse the global/application properties.
+ // Creating a window context is expensive, therefore we avoid it.
+ DisplayWindowProperties(
+ displayId = displayId,
+ windowType = windowType,
+ context = globalContext,
+ windowManager = globalWindowManager,
+ )
+ } else {
+ val context = createWindowContext(display, windowType)
+ @SuppressLint("NonInjectedService") // Need to manually get the service
+ val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager
+ DisplayWindowProperties(displayId, windowType, context, windowManager)
+ }
+ }
+
+ private fun createWindowContext(display: Display, windowType: Int): Context =
+ globalContext.createWindowContext(display, windowType, /* options= */ null).also {
+ it.setTheme(R.style.Theme_SystemUI)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.write("perDisplayContexts: $properties")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt
new file mode 100644
index 000000000000..6acc296367a9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.shared.model
+
+import android.content.Context
+import android.view.WindowManager
+
+/** Represents a display specific group of window related properties. */
+data class DisplayWindowProperties(
+ /** The id of the display associated with this instance. */
+ val displayId: Int,
+ /**
+ * The window type that was used to create the [Context] in this instance, using
+ * [Context.createWindowContext]. This is the window type that can be used when adding views to
+ * the [WindowManager] associated with this instance.
+ */
+ @WindowManager.LayoutParams.WindowType val windowType: Int,
+ /**
+ * The display specific [Context] created using [Context.createWindowContext] with window type
+ * associated with this instance.
+ */
+ val context: Context,
+
+ /**
+ * The display specific [WindowManager] instance to be used when adding windows of the type
+ * associated with this instance.
+ */
+ val windowManager: WindowManager,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
index d5ff8f21abb2..2b617525fbcc 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
@@ -39,8 +39,10 @@ class DreamOverlayCallbackController @Inject constructor() :
}
fun onWakeUp() {
- isDreaming = false
- callbacks.forEach { it.onWakeUp() }
+ if (isDreaming) {
+ isDreaming = false
+ callbacks.forEach { it.onWakeUp() }
+ }
}
fun onStartDream() {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 83f86a718029..7a6ca0859a09 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -297,6 +297,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
mStateController.setLowLightActive(false);
mStateController.setEntryAnimationsFinished(false);
+ mDreamOverlayCallbackController.onWakeUp();
+
if (mDreamOverlayContainerViewController != null) {
mDreamOverlayContainerViewController.destroy();
mDreamOverlayContainerViewController = null;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 2052459692b2..d28b08f83a4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2460,6 +2460,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
return;
}
+
+ if (mKeyguardStateController.isKeyguardGoingAway()) {
+ Log.i(TAG, "Ignoring dismiss because we're already going away.");
+ return;
+ }
+
mHandler.obtainMessage(DISMISS, new DismissMessage(callback, message)).sendToTarget();
}
@@ -3428,6 +3434,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
return;
}
+ if (mIsKeyguardExitAnimationCanceled) {
+ Log.d(TAG, "Ignoring exitKeyguardAndFinishSurfaceBehindRemoteAnimation. "
+ + "mIsKeyguardExitAnimationCanceled==true");
+ return;
+ }
+
// Block the panel from expanding, in case we were doing a swipe to dismiss gesture.
mKeyguardViewControllerLazy.get().blockPanelExpansionFromCurrentTouch();
final boolean wasShowing = mShowing;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 65b42e657e75..fcf486b5696b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -23,6 +23,7 @@ import com.android.systemui.back.domain.interactor.BackActionInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler.Companion.handleAction
import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper
+import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.ShadeController
@@ -105,7 +106,15 @@ constructor(
(statusBarStateController.state != StatusBarState.SHADE) &&
statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
if (shouldUnlockOnMenuPressed) {
- shadeController.animateCollapseShadeForced()
+ statusBarKeyguardViewManager.dismissWithAction(
+ object : OnDismissAction {
+ override fun onDismiss(): Boolean {
+ return false
+ }
+ },
+ null,
+ false,
+ )
return true
}
return false
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index c4f231dfc012..a0000f3c66fd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor
import android.annotation.SuppressLint
import android.util.Log
+import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -37,15 +38,22 @@ import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
@@ -206,44 +214,155 @@ constructor(
)
}
- return if (SceneContainerFlag.isEnabled) {
- flow.filter { step ->
- val fromScene =
- when (edge) {
- is Edge.StateToState -> edge.from?.mapToSceneContainerScene()
- is Edge.StateToScene -> edge.from?.mapToSceneContainerScene()
- is Edge.SceneToState -> edge.from
- }
+ if (!SceneContainerFlag.isEnabled) {
+ return flow
+ }
+ if (edge.isSceneWildcardEdge()) {
+ return simulateTransitionStepsForSceneTransitions(edge)
+ }
+ return flow.filter { step ->
+ val fromScene =
+ when (edge) {
+ is Edge.StateToState -> edge.from?.mapToSceneContainerScene()
+ is Edge.StateToScene -> edge.from?.mapToSceneContainerScene()
+ is Edge.SceneToState -> edge.from
+ }
+
+ val toScene =
+ when (edge) {
+ is Edge.StateToState -> edge.to?.mapToSceneContainerScene()
+ is Edge.StateToScene -> edge.to
+ is Edge.SceneToState -> edge.to?.mapToSceneContainerScene()
+ }
+
+ val isTransitioningBetweenLockscreenStates =
+ fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull()
+ val isTransitioningBetweenDesiredScenes =
+ sceneInteractor.transitionState.value.isTransitioning(fromScene, toScene)
+
+ // We can't compare the terminal step with the current sceneTransition because
+ // a) STL has no guarantee that it will settle in Idle() when finished/canceled
+ // b) Comparing to Idle(toScene) would make any other FINISHED step settling in
+ // toScene pass as well
+ val terminalStepBelongsToPreviousTransition =
+ (step.transitionState == TransitionState.FINISHED ||
+ step.transitionState == TransitionState.CANCELED) &&
+ sceneTransitionPair.value.previousValue.isTransitioning(fromScene, toScene)
+
+ return@filter isTransitioningBetweenLockscreenStates ||
+ isTransitioningBetweenDesiredScenes ||
+ terminalStepBelongsToPreviousTransition
+ }
+ }
- val toScene =
- when (edge) {
- is Edge.StateToState -> edge.to?.mapToSceneContainerScene()
- is Edge.StateToScene -> edge.to
- is Edge.SceneToState -> edge.to?.mapToSceneContainerScene()
+ private fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null
+
+ /**
+ * This function will return a flow that simulates TransitionSteps based on STL movements
+ * filtered by [edge].
+ *
+ * STL transitions outside of Lockscreen Transitions are not tracked in KTI. This is an issue
+ * for wildcard edges, as this means that Scenes.Bouncer -> Scenes.Gone would not appear while
+ * AOD -> Scenes.Bouncer would appear.
+ *
+ * This function will track STL transitions only when a wildcard edge is provided and emit a
+ * RUNNING step for each update to [Transition.progress]. It will also emit a STARTED and
+ * FINISHED step when the transitions starts and finishes.
+ *
+ * All TransitionSteps will have UNDEFINED as to and from state even when one of them is the
+ * Lockscreen Scene. It indicates that both are scenes but it should not be relevant to
+ * consumers of the [transition] API as usually all viewModels are just interested in the
+ * progress value. The correct filtering based on the provided [edge] is always the
+ * responsibility of KTI and therefore only proper [TransitionStep]s are emitted. The filter is
+ * applied within this function.
+ */
+ private fun simulateTransitionStepsForSceneTransitions(edge: Edge) =
+ sceneInteractor.transitionState.flatMapLatestWithFinished {
+ when (it) {
+ is ObservableTransitionState.Idle -> {
+ flowOf()
+ }
+ is ObservableTransitionState.Transition -> {
+ val isMatchingTransition =
+ when (edge) {
+ is Edge.StateToState ->
+ throw IllegalStateException("Should not be reachable.")
+ is Edge.SceneToState -> it.isTransitioning(from = edge.from)
+ is Edge.StateToScene -> it.isTransitioning(to = edge.to)
+ }
+ if (!isMatchingTransition) {
+ return@flatMapLatestWithFinished flowOf()
+ }
+ flow {
+ emit(
+ TransitionStep(
+ from = UNDEFINED,
+ to = UNDEFINED,
+ value = 0f,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ emitAll(
+ it.progress.map { progress ->
+ TransitionStep(
+ from = UNDEFINED,
+ to = UNDEFINED,
+ value = progress,
+ transitionState = TransitionState.RUNNING,
+ )
+ }
+ )
}
+ }
+ }
+ }
- fun SceneKey?.isLockscreenOrNull() = this == Scenes.Lockscreen || this == null
-
- val isTransitioningBetweenLockscreenStates =
- fromScene.isLockscreenOrNull() && toScene.isLockscreenOrNull()
- val isTransitioningBetweenDesiredScenes =
- sceneInteractor.transitionState.value.isTransitioning(fromScene, toScene)
-
- // We can't compare the terminal step with the current sceneTransition because
- // a) STL has no guarantee that it will settle in Idle() when finished/canceled
- // b) Comparing to Idle(toScene) would make any other FINISHED step settling in
- // toScene pass as well
- val terminalStepBelongsToPreviousTransition =
- (step.transitionState == TransitionState.FINISHED ||
- step.transitionState == TransitionState.CANCELED) &&
- sceneTransitionPair.value.previousValue.isTransitioning(fromScene, toScene)
-
- return@filter isTransitioningBetweenLockscreenStates ||
- isTransitioningBetweenDesiredScenes ||
- terminalStepBelongsToPreviousTransition
+ /**
+ * This function is similar to flatMapLatest but it will additionally emit a FINISHED
+ * TransitionStep whenever the flattened innerFlow emitted a STARTED step and is now being
+ * replaced by a new innerFlow.
+ *
+ * This is to make sure that every STARTED step will receive a corresponding FINISHED step.
+ *
+ * We can't simply write this into a flow {} block because Transition.progress doesn't complete.
+ * We also can't emit the FINISHED step simply when an Idle state is reached because a)
+ * Transitions are not guaranteed to finish in Idle and b) There can be multiple Idle
+ * transitions after another
+ */
+ private fun <T> Flow<T>.flatMapLatestWithFinished(
+ transform: suspend (T) -> Flow<TransitionStep>
+ ): Flow<TransitionStep> = channelFlow {
+ var job: Job? = null
+ var startedEmitted = false
+
+ coroutineScope {
+ collect { value ->
+ job?.cancelAndJoin()
+
+ job = launch {
+ val innerFlow = transform(value)
+ try {
+ innerFlow.collect { step ->
+ if (step.transitionState == TransitionState.STARTED) {
+ startedEmitted = true
+ }
+ send(step)
+ }
+ } finally {
+ if (startedEmitted) {
+ send(
+ TransitionStep(
+ from = UNDEFINED,
+ to = UNDEFINED,
+ value = 1f,
+ transitionState = TransitionState.FINISHED,
+ )
+ )
+ startedEmitted = false
+ }
+ }
+ }
}
- } else {
- flow
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt
index ef7e7eb59898..62694ceffda8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt
@@ -22,13 +22,18 @@ import com.android.systemui.qs.tileimpl.QSTileImpl
/**
* Creates a [QSTile.Icon] from an [Icon].
- * * [Icon.Loaded] -> [QSTileImpl.DrawableIcon]
+ * * [Icon.Loaded] && [resId] null -> [QSTileImpl.DrawableIcon]
+ * * [Icon.Loaded] && [resId] available -> [QSTileImpl.DrawableIconWithRes]
* * [Icon.Resource] -> [QSTileImpl.ResourceIcon]
*/
-fun Icon.asQSTileIcon(): QSTile.Icon {
+fun Icon.asQSTileIcon(resId: Int?): QSTile.Icon {
return when (this) {
is Icon.Loaded -> {
- QSTileImpl.DrawableIcon(this.drawable)
+ if (resId != null) {
+ QSTileImpl.DrawableIconWithRes(this.drawable, resId)
+ } else {
+ QSTileImpl.DrawableIcon(this.drawable)
+ }
}
is Icon.Resource -> {
QSTileImpl.ResourceIcon.get(this.res)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 65c29b829429..9c5231d716da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -92,6 +92,7 @@ import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings
import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
+import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
import com.android.systemui.qs.composefragment.ui.notificationScrimClip
import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -149,20 +150,12 @@ constructor(
private val notificationScrimClippingParams =
object {
var isEnabled by mutableStateOf(false)
- var leftInset by mutableStateOf(0)
- var rightInset by mutableStateOf(0)
- var top by mutableStateOf(0)
- var bottom by mutableStateOf(0)
- var radius by mutableStateOf(0)
+ var params by mutableStateOf(NotificationScrimClipParams())
fun dump(pw: IndentingPrintWriter) {
pw.printSection("NotificationScrimClippingParams") {
pw.println("isEnabled", isEnabled)
- pw.println("leftInset", "${leftInset}px")
- pw.println("rightInset", "${rightInset}px")
- pw.println("top", "${top}px")
- pw.println("bottom", "${bottom}px")
- pw.println("radius", "${radius}px")
+ pw.println("params", params)
}
}
}
@@ -216,7 +209,7 @@ constructor(
FrameLayoutTouchPassthrough(
context,
{ notificationScrimClippingParams.isEnabled },
- { notificationScrimClippingParams.top },
+ { notificationScrimClippingParams.params.top },
)
frame.addView(
composeView,
@@ -237,13 +230,7 @@ constructor(
Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
notificationScrimClippingParams.isEnabled
) {
- Modifier.notificationScrimClip(
- notificationScrimClippingParams.leftInset,
- notificationScrimClippingParams.top,
- notificationScrimClippingParams.rightInset,
- notificationScrimClippingParams.bottom,
- notificationScrimClippingParams.radius,
- )
+ Modifier.notificationScrimClip { notificationScrimClippingParams.params }
},
) {
val isEditing by
@@ -445,13 +432,14 @@ constructor(
fullWidth: Boolean,
) {
notificationScrimClippingParams.isEnabled = visible
- notificationScrimClippingParams.top = top
- notificationScrimClippingParams.bottom = bottom
- // Full width means that QS will show in the entire width allocated to it (for example
- // phone) vs. showing in a narrower column (for example, tablet portrait).
- notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset
- notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset
- notificationScrimClippingParams.radius = cornerRadius
+ notificationScrimClippingParams.params =
+ NotificationScrimClipParams(
+ top,
+ bottom,
+ if (fullWidth) 0 else leftInset,
+ if (fullWidth) 0 else rightInset,
+ cornerRadius,
+ )
}
override fun isFullyCollapsed(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
index 93c6445b78ef..c912bd59c19f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
@@ -31,87 +31,73 @@ import androidx.compose.ui.platform.InspectorInfo
* ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
* from the QS container.
*/
-fun Modifier.notificationScrimClip(
- leftInset: Int,
- top: Int,
- rightInset: Int,
- bottom: Int,
- radius: Int
-): Modifier {
- return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius)
+fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
+ return this then NotificationScrimClipElement(clipParams)
}
-private class NotificationScrimClipNode(
- var leftInset: Float,
- var top: Float,
- var rightInset: Float,
- var bottom: Float,
- var radius: Float,
-) : DrawModifierNode, Modifier.Node() {
+private class NotificationScrimClipNode(var clipParams: () -> NotificationScrimClipParams) :
+ DrawModifierNode, Modifier.Node() {
private val path = Path()
- var invalidated = true
+ private var lastClipParams = NotificationScrimClipParams()
override fun ContentDrawScope.draw() {
- if (invalidated) {
+ val newClipParams = clipParams()
+ if (newClipParams != lastClipParams) {
+ lastClipParams = newClipParams
+ applyClipParams(path, lastClipParams)
+ }
+ clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
+ }
+
+ private fun ContentDrawScope.applyClipParams(
+ path: Path,
+ clipParams: NotificationScrimClipParams,
+ ) {
+ with(clipParams) {
path.rewind()
path
.asAndroidPath()
.addRoundRect(
- -leftInset,
- top,
+ -leftInset.toFloat(),
+ top.toFloat(),
size.width + rightInset,
- bottom,
- radius,
- radius,
- android.graphics.Path.Direction.CW
+ bottom.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ android.graphics.Path.Direction.CW,
)
- invalidated = false
}
- clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
}
}
-private data class NotificationScrimClipElement(
- val leftInset: Int,
- val top: Int,
- val rightInset: Int,
- val bottom: Int,
- val radius: Int,
-) : ModifierNodeElement<NotificationScrimClipNode>() {
+private data class NotificationScrimClipElement(val clipParams: () -> NotificationScrimClipParams) :
+ ModifierNodeElement<NotificationScrimClipNode>() {
override fun create(): NotificationScrimClipNode {
- return NotificationScrimClipNode(
- leftInset.toFloat(),
- top.toFloat(),
- rightInset.toFloat(),
- bottom.toFloat(),
- radius.toFloat(),
- )
+ return NotificationScrimClipNode(clipParams)
}
override fun update(node: NotificationScrimClipNode) {
- val changed =
- node.leftInset != leftInset.toFloat() ||
- node.top != top.toFloat() ||
- node.rightInset != rightInset.toFloat() ||
- node.bottom != bottom.toFloat() ||
- node.radius != radius.toFloat()
- if (changed) {
- node.leftInset = leftInset.toFloat()
- node.top = top.toFloat()
- node.rightInset = rightInset.toFloat()
- node.bottom = bottom.toFloat()
- node.radius = radius.toFloat()
- node.invalidated = true
- }
+ node.clipParams = clipParams
}
override fun InspectorInfo.inspectableProperties() {
name = "notificationScrimClip"
- properties["leftInset"] = leftInset
- properties["top"] = top
- properties["rightInset"] = rightInset
- properties["bottom"] = bottom
- properties["radius"] = radius
+ with(clipParams()) {
+ properties["leftInset"] = leftInset
+ properties["top"] = top
+ properties["rightInset"] = rightInset
+ properties["bottom"] = bottom
+ properties["radius"] = radius
+ }
}
}
+
+/** Params for [notificationScrimClip]. */
+data class NotificationScrimClipParams(
+ val top: Int = 0,
+ val bottom: Int = 0,
+ val leftInset: Int = 0,
+ val rightInset: Int = 0,
+ val radius: Int = 0,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
index cf2db6c66ce7..3bbe624595d9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -121,7 +121,7 @@ constructor(
state?.apply {
this.state = tileState.activationState.legacyState
val tileStateIcon = tileState.icon()
- icon = tileStateIcon?.asQSTileIcon() ?: ResourceIcon.get(ICON_RES_ID)
+ icon = tileStateIcon?.asQSTileIcon(tileState.iconRes) ?: ResourceIcon.get(ICON_RES_ID)
label = tileLabel
secondaryLabel = tileState.secondaryLabel
contentDescription = tileState.contentDescription
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 5d44ead7c21a..40591bf56e0a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -76,14 +76,14 @@ constructor(
} else {
return ModesTileModel(
isActivated = activeModes.isAnyActive(),
- icon = context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(),
+ icon = Icon.Resource(ModesTile.ICON_RES_ID, null),
iconResId = ModesTile.ICON_RES_ID,
activeModes = activeModes.modeNames,
)
}
}
- private data class TileIcon(val icon: Icon.Loaded, val resId: Int?)
+ private data class TileIcon(val icon: Icon, val resId: Int?)
private fun getTileIcon(activeMode: ZenModeInfo?): TileIcon {
return if (activeMode != null) {
@@ -94,7 +94,7 @@ constructor(
TileIcon(activeMode.icon.drawable.asIcon(), null)
}
} else {
- TileIcon(context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), ModesTile.ICON_RES_ID)
+ TileIcon(Icon.Resource(ModesTile.ICON_RES_ID, null), ModesTile.ICON_RES_ID)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
index db4812342050..9c31e322dfd2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt
@@ -21,12 +21,12 @@ import com.android.systemui.common.shared.model.Icon
data class ModesTileModel(
val isActivated: Boolean,
val activeModes: List<String>,
- val icon: Icon.Loaded,
+ val icon: Icon,
/**
* Resource id corresponding to [icon]. Will only be present if it's know to correspond to a
* resource with a known id in SystemUI (such as resources from `android.R`,
* `com.android.internal.R`, or `com.android.systemui.res` itself).
*/
- val iconResId: Int? = null
+ val iconResId: Int? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 69da3134314b..801a0ce4b744 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -18,7 +18,9 @@ package com.android.systemui.qs.tiles.impl.modes.ui
import android.content.res.Resources
import android.icu.text.MessageFormat
+import android.util.Log
import android.widget.Button
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
@@ -30,14 +32,30 @@ import javax.inject.Inject
class ModesTileMapper
@Inject
-constructor(
- @Main private val resources: Resources,
- val theme: Resources.Theme,
-) : QSTileDataToStateMapper<ModesTileModel> {
+constructor(@Main private val resources: Resources, val theme: Resources.Theme) :
+ QSTileDataToStateMapper<ModesTileModel> {
override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState =
QSTileState.build(resources, theme, config.uiConfig) {
- iconRes = data.iconResId
- icon = { data.icon }
+ val loadedIcon: Icon.Loaded =
+ when (val dataIcon = data.icon) {
+ is Icon.Resource -> {
+ if (iconRes != dataIcon.res) {
+ Log.wtf(
+ "ModesTileMapper",
+ "Icon.Resource.res & iconResId are not identical",
+ )
+ }
+ iconRes = dataIcon.res
+ Icon.Loaded(resources.getDrawable(dataIcon.res, theme), null)
+ }
+ is Icon.Loaded -> {
+ iconRes = data.iconResId
+ dataIcon
+ }
+ }
+
+ icon = { loadedIcon }
+
activationState =
if (data.isActivated) {
QSTileState.ActivationState.ACTIVE
@@ -47,10 +65,7 @@ constructor(
secondaryLabel = getModesStatus(data, resources)
contentDescription = "$label. $secondaryLabel"
supportedActions =
- setOf(
- QSTileState.UserAction.CLICK,
- QSTileState.UserAction.LONG_CLICK,
- )
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
sideViewIcon = QSTileState.SideViewIcon.Chevron
expandedAccessibilityClass = Button::class
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 73ad0e50793a..da04f6edf9e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -46,6 +46,7 @@ import com.android.internal.jank.InteractionJankMonitor.Configuration;
import com.android.internal.logging.UiEventLogger;
import com.android.keyguard.KeyguardClockSwitch;
import com.android.systemui.DejankUtils;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
@@ -123,6 +124,7 @@ public class StatusBarStateControllerImpl implements
private final Lazy<SceneContainerOcclusionInteractor> mSceneContainerOcclusionInteractorLazy;
private final Lazy<KeyguardClockInteractor> mKeyguardClockInteractorLazy;
private final Lazy<SceneBackInteractor> mSceneBackInteractorLazy;
+ private final Lazy<AlternateBouncerInteractor> mAlternateBouncerInteractorLazy;
private int mState;
private int mLastState;
private int mUpcomingState;
@@ -193,7 +195,8 @@ public class StatusBarStateControllerImpl implements
Lazy<SceneInteractor> sceneInteractorLazy,
Lazy<SceneContainerOcclusionInteractor> sceneContainerOcclusionInteractor,
Lazy<KeyguardClockInteractor> keyguardClockInteractorLazy,
- Lazy<SceneBackInteractor> sceneBackInteractorLazy) {
+ Lazy<SceneBackInteractor> sceneBackInteractorLazy,
+ Lazy<AlternateBouncerInteractor> alternateBouncerInteractorLazy) {
mUiEventLogger = uiEventLogger;
mInteractionJankMonitorLazy = interactionJankMonitorLazy;
mJavaAdapter = javaAdapter;
@@ -205,6 +208,7 @@ public class StatusBarStateControllerImpl implements
mSceneContainerOcclusionInteractorLazy = sceneContainerOcclusionInteractor;
mKeyguardClockInteractorLazy = keyguardClockInteractorLazy;
mSceneBackInteractorLazy = sceneBackInteractorLazy;
+ mAlternateBouncerInteractorLazy = alternateBouncerInteractorLazy;
for (int i = 0; i < HISTORY_SIZE; i++) {
mHistoricalRecords[i] = new HistoricalState();
}
@@ -233,6 +237,7 @@ public class StatusBarStateControllerImpl implements
mSceneInteractorLazy.get().getCurrentOverlays(),
mSceneBackInteractorLazy.get().getBackStack(),
mSceneContainerOcclusionInteractorLazy.get().getInvisibleDueToOcclusion(),
+ mAlternateBouncerInteractorLazy.get().isVisible(),
this::calculateStateFromSceneFramework),
this::onStatusBarStateChanged);
@@ -693,7 +698,8 @@ public class StatusBarStateControllerImpl implements
SceneKey currentScene,
Set<OverlayKey> currentOverlays,
SceneStack backStack,
- boolean isOccluded) {
+ boolean isOccluded,
+ boolean alternateBouncerIsVisible) {
SceneContainerFlag.isUnexpectedlyInLegacyMode();
final boolean onBouncer = currentScene.equals(Scenes.Bouncer);
@@ -714,7 +720,8 @@ public class StatusBarStateControllerImpl implements
final String inputLogString = "currentScene=" + currentScene.getTestTag()
+ " currentOverlays=" + currentOverlays + " backStack=" + backStack
- + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded;
+ + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded
+ + " alternateBouncerIsVisible=" + alternateBouncerIsVisible;
int newState;
@@ -722,6 +729,7 @@ public class StatusBarStateControllerImpl implements
// 1. deviceUnlockStatus.isUnlocked changes from false to true.
// 2. Lockscreen changes to Gone, either in currentScene or in backStack.
// 3. Bouncer is removed from currentScene or backStack, if it was present.
+ // 4. the alternate bouncer is hidden, if it was visible.
//
// From this function's perspective, though, deviceUnlockStatus, currentScene, and backStack
// each update separately, and the relative order of those updates is not well-defined. This
@@ -733,6 +741,7 @@ public class StatusBarStateControllerImpl implements
// 1. deviceUnlockStatus.isUnlocked is false.
// 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal).
// 3. backStack contains a keyguardish scene (Lockscreen or Communal).
+ // 4. the alternate bouncer is visible.
final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal;
final boolean overKeyguardish = overLockscreen || overCommunal;
@@ -741,7 +750,7 @@ public class StatusBarStateControllerImpl implements
// Occlusion is special; even though the device is still technically on the lockscreen,
// the UI behaves as if it is unlocked.
newState = StatusBarState.SHADE;
- } else if (onKeyguardish || overKeyguardish) {
+ } else if (onKeyguardish || overKeyguardish || alternateBouncerIsVisible) {
// We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we
// want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a
// keyguardish scene.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index dac01028ef64..1009028345de 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.connectivity
import android.os.UserManager
+import com.android.systemui.bluetooth.qsdialog.dagger.AudioSharingModule
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION
import com.android.systemui.qs.QsEventLogger
@@ -56,7 +57,7 @@ import dagger.Provides
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey
-@Module
+@Module(includes = [AudioSharingModule::class])
interface ConnectivityModule {
/** Inject BluetoothTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index cf238d553225..cd1642eee4b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -22,15 +22,20 @@ import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.StatusBarDataLayerModule
import com.android.systemui.statusbar.phone.LightBarController
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
+import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
+import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerImpl
+import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import dagger.Binds
+import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
@@ -62,13 +67,19 @@ abstract class StatusBarModule {
@ClassKey(StatusBarSignalPolicy::class)
abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable
+ @Binds
+ @SysUISingleton
+ abstract fun statusBarWindowControllerFactory(
+ implFactory: StatusBarWindowControllerImpl.Factory
+ ): StatusBarWindowController.Factory
+
companion object {
@Provides
@SysUISingleton
- fun statusBarWindowController(
- context: Context?,
- viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager?,
+ fun defaultStatusBarWindowController(
+ context: Context,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
factory: StatusBarWindowControllerImpl.Factory,
): StatusBarWindowController {
return factory.create(context, viewCaptureAwareWindowManager)
@@ -76,6 +87,33 @@ abstract class StatusBarModule {
@Provides
@SysUISingleton
+ fun windowControllerStore(
+ multiDisplayImplLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>,
+ singleDisplayImplLazy: Lazy<SingleDisplayStatusBarWindowControllerStore>,
+ ): StatusBarWindowControllerStore {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ multiDisplayImplLazy.get()
+ } else {
+ singleDisplayImplLazy.get()
+ }
+ }
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(MultiDisplayStatusBarWindowControllerStore::class)
+ fun multiDisplayControllerStoreAsCoreStartable(
+ storeLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>
+ ): CoreStartable {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ storeLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
+
+ @Provides
+ @SysUISingleton
@OngoingCallLog
fun provideOngoingCallLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("OngoingCall", 75)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 7b6a2cb62b14..560028cb5640 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
@@ -444,9 +444,11 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
if (onFinishedRunnable != null) {
onFinishedRunnable.run();
}
+ if (mRunWithoutInterruptions) {
+ enableAppearDrawing(false);
+ }
// We need to reset the View state, even if the animation was cancelled
- enableAppearDrawing(false);
onAppearAnimationFinished(isAppearing);
if (mRunWithoutInterruptions) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 0ad22e0b6dc9..f39af18afcea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -68,6 +68,8 @@ import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
@@ -75,6 +77,7 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.systemui.util.kotlin.sample
+import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -137,8 +140,10 @@ constructor(
private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
private val primaryBouncerToLockscreenTransitionViewModel:
PrimaryBouncerToLockscreenTransitionViewModel,
- private val aodBurnInViewModel: AodBurnInViewModel,
+ aodBurnInViewModel: AodBurnInViewModel,
private val communalSceneInteractor: CommunalSceneInteractor,
+ // Lazy because it's only used in the SceneContainer + Dual Shade configuration.
+ headsUpNotificationInteractor: Lazy<HeadsUpNotificationInteractor>,
unfoldTransitionInteractor: UnfoldTransitionInteractor,
) : FlowDumperImpl(dumpManager) {
@@ -390,20 +395,36 @@ constructor(
* notifications unless in splitshade.
*/
private val alphaForShadeAndQsExpansion: Flow<Float> =
- interactor.configurationBasedDimensions
- .flatMapLatest { configurationBasedDimensions ->
- combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) {
- shadeExpansion,
- qsExpansion ->
- if (shadeExpansion > 0f || qsExpansion > 0f) {
- if (configurationBasedDimensions.useSplitShade) {
- emit(1f)
- } else if (qsExpansion == 1f) {
- // Ensure HUNs will be visible in QS shade (at least while unlocked)
- emit(1f)
- } else {
- // Fade as QS shade expands
- emit(1f - qsExpansion)
+ if (DualShade.isEnabled) {
+ combineTransform(
+ headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway,
+ shadeInteractor.shadeExpansion,
+ shadeInteractor.qsExpansion,
+ ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion ->
+ if (isHeadsUpOrAnimatingAway) {
+ // Ensure HUNs will be visible in QS shade (at least while unlocked)
+ emit(1f)
+ } else if (shadeExpansion > 0f || qsExpansion > 0f) {
+ // Fade out as QS shade expands
+ emit(1f - qsExpansion)
+ }
+ }
+ } else {
+ interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions
+ ->
+ combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) {
+ shadeExpansion,
+ qsExpansion ->
+ if (shadeExpansion > 0f || qsExpansion > 0f) {
+ if (configurationBasedDimensions.useSplitShade) {
+ emit(1f)
+ } else if (qsExpansion == 1f) {
+ // Ensure HUNs will be visible in QS shade (at least while unlocked)
+ emit(1f)
+ } else {
+ // Fade as QS shade expands
+ emit(1f - qsExpansion)
+ }
}
}
}
@@ -427,7 +448,7 @@ constructor(
private fun alphaForTransitions(viewState: ViewStateAccessor): Flow<Float> {
return merge(
keyguardInteractor.dismissAlpha.dumpWhileCollecting("keyguardInteractor.dismissAlpha"),
- // All transition view models are mututally exclusive, and safe to merge
+ // All transition view models are mutually exclusive, and safe to merge
bouncerToGoneNotificationAlpha(viewState),
aodToGoneTransitionViewModel.notificationAlpha(viewState),
aodToLockscreenTransitionViewModel.notificationAlpha,
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 0474344ee390..7e5b45543e9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -33,7 +33,6 @@ import static com.android.systemui.Flags.relockWithPowerButtonImmediately;
import static com.android.systemui.Flags.statusBarSignalPolicyRefactor;
import static com.android.systemui.charging.WirelessChargingAnimation.UNKNOWN_BATTERY_LEVEL;
import static com.android.systemui.flags.Flags.SHORTCUT_LIST_SEARCH_LAYOUT;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.PERMISSION_SELF;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import android.annotation.Nullable;
@@ -41,7 +40,6 @@ import android.app.ActivityOptions;
import android.app.IWallpaperManager;
import android.app.KeyguardManager;
import android.app.Notification;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.app.TaskInfo;
@@ -275,11 +273,6 @@ import javax.inject.Provider;
@SysUISingleton
public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
- private static final String BANNER_ACTION_CANCEL =
- "com.android.systemui.statusbar.banner_action_cancel";
- private static final String BANNER_ACTION_SETUP =
- "com.android.systemui.statusbar.banner_action_setup";
-
private static final int MSG_LAUNCH_TRANSITION_TIMEOUT = 1003;
// 1020-1040 reserved for BaseStatusBar
@@ -963,12 +956,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
}
- IntentFilter internalFilter = new IntentFilter();
- internalFilter.addAction(BANNER_ACTION_CANCEL);
- internalFilter.addAction(BANNER_ACTION_SETUP);
- mContext.registerReceiver(mBannerActionBroadcastReceiver, internalFilter, PERMISSION_SELF,
- null, Context.RECEIVER_EXPORTED_UNAUDITED);
-
if (mWallpaperSupported) {
IWallpaperManager wallpaperManager = IWallpaperManager.Stub.asInterface(
ServiceManager.getService(Context.WALLPAPER_SERVICE));
@@ -2948,29 +2935,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
return mDeviceInteractive;
}
- private final BroadcastReceiver mBannerActionBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (BANNER_ACTION_CANCEL.equals(action) || BANNER_ACTION_SETUP.equals(action)) {
- NotificationManager noMan = (NotificationManager)
- mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- noMan.cancel(com.android.internal.messages.nano.SystemMessageProto.SystemMessage.
- NOTE_HIDDEN_NOTIFICATIONS);
-
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_NOTE_ABOUT_NOTIFICATION_HIDING, 0);
- if (BANNER_ACTION_SETUP.equals(action)) {
- mShadeController.animateCollapseShadeForced();
- mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_REDACTION)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-
- );
- }
- }
- }
- };
-
@Override
public void handleExternalShadeWindowTouch(MotionEvent event) {
getNotificationShadeWindowViewController().handleExternalTouch(event);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index bef552ce7769..ff7c14308fcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -151,7 +151,11 @@ private constructor(
startSideContainer = mView.requireViewById(R.id.status_bar_start_side_content)
startSideContainer.setOnHoverListener(
- statusOverlayHoverListenerFactory.createDarkAwareListener(startSideContainer)
+ statusOverlayHoverListenerFactory.createDarkAwareListener(
+ startSideContainer,
+ topHoverMargin = 6,
+ bottomHoverMargin = 6,
+ )
)
startSideContainer.setOnTouchListener(iconsOnTouchListener)
}
@@ -210,7 +214,7 @@ private constructor(
event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL
centralSurfaces.setInteracting(
WINDOW_STATUS_BAR,
- !upOrCancel || shadeController.isExpandedVisible
+ !upOrCancel || shadeController.isExpandedVisible,
)
}
}
@@ -247,7 +251,7 @@ private constructor(
String.format(
"onTouchForwardedFromStatusBar: panel disabled, " +
"ignoring touch at (${event.x.toInt()},${event.y.toInt()})"
- )
+ ),
)
}
return false
@@ -266,7 +270,7 @@ private constructor(
if (!shadeViewController.isViewEnabled) {
shadeLogger.logMotionEvent(
event,
- "onTouchForwardedFromStatusBar: panel view disabled"
+ "onTouchForwardedFromStatusBar: panel view disabled",
)
return true
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 479ffb728eb2..17bd53869ee5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -530,6 +530,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
this::consumeKeyguardAuthenticatedBiometricsHandled
);
} else {
+ // Collector that keeps the AlternateBouncerInteractor#canShowAlternateBouncer flow hot.
mListenForCanShowAlternateBouncer = mJavaAdapter.alwaysCollectFlow(
mAlternateBouncerInteractor.getCanShowAlternateBouncer(),
this::consumeCanShowAlternateBouncer
@@ -578,8 +579,17 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
}
private void consumeCanShowAlternateBouncer(boolean canShow) {
- // do nothing, we only are registering for the flow to ensure that there's at least
- // one subscriber that will update AlternateBouncerInteractor.canShowAlternateBouncer.value
+ // Hack: this is required to fix issues where
+ // KeyguardBouncerRepository#alternateBouncerVisible state is incorrectly set and then never
+ // reset. This is caused by usages of show()/forceShow() that only read this flow to set the
+ // alternate bouncer visible state, if there is a race condition between when that flow
+ // changes to false and when the read happens, the flow will be set to an incorrect value
+ // and not reset on time.
+ if (!canShow) {
+ Log.d(TAG, "canShowAlternateBouncer turned false, maybe try hiding the alternate "
+ + "bouncer if it is already visible");
+ mAlternateBouncerInteractor.maybeHide();
+ }
}
/** Register a callback, to be invoked by the Predictive Back system. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt
index 640ec281e162..c40822d71952 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusOverlayHoverListener.kt
@@ -20,6 +20,7 @@ import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.PaintDrawable
+import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.View.OnHoverListener
@@ -27,10 +28,10 @@ import androidx.annotation.ColorInt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.res.R
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
@@ -65,6 +66,26 @@ constructor(
createDarkAwareListener(view, darkIconDispatcher.darkChangeFlow())
/**
+ * Creates listener using [DarkIconDispatcher] to determine light or dark color of the overlay
+ * Also sets margins for hover background relative to view bounds
+ */
+ fun createDarkAwareListener(
+ view: View,
+ leftHoverMargin: Int = 0,
+ rightHoverMargin: Int = 0,
+ topHoverMargin: Int = 0,
+ bottomHoverMargin: Int = 0,
+ ) =
+ createDarkAwareListener(
+ view,
+ darkIconDispatcher.darkChangeFlow(),
+ leftHoverMargin,
+ rightHoverMargin,
+ topHoverMargin,
+ bottomHoverMargin,
+ )
+
+ /**
* Creates listener using provided [DarkChange] producer to determine light or dark color of the
* overlay
*/
@@ -76,6 +97,25 @@ constructor(
darkFlow.map { toHoverTheme(view, it) },
)
+ private fun createDarkAwareListener(
+ view: View,
+ darkFlow: StateFlow<DarkChange>,
+ leftHoverMargin: Int = 0,
+ rightHoverMargin: Int = 0,
+ topHoverMargin: Int = 0,
+ bottomHoverMargin: Int = 0,
+ ) =
+ StatusOverlayHoverListener(
+ view,
+ configurationController,
+ resources,
+ darkFlow.map { toHoverTheme(view, it) },
+ leftHoverMargin,
+ rightHoverMargin,
+ topHoverMargin,
+ bottomHoverMargin,
+ )
+
private fun toHoverTheme(view: View, darkChange: DarkChange): HoverTheme {
val calculatedTint = DarkIconDispatcher.getTint(darkChange.areas, view, darkChange.tint)
// currently calculated tint is either white or some shade of black.
@@ -91,7 +131,7 @@ constructor(
*/
enum class HoverTheme {
LIGHT,
- DARK
+ DARK,
}
/**
@@ -103,11 +143,19 @@ class StatusOverlayHoverListener(
configurationController: ConfigurationController,
private val resources: Resources,
private val themeFlow: Flow<HoverTheme>,
+ private val leftHoverMargin: Int = 0,
+ private val rightHoverMargin: Int = 0,
+ private val topHoverMargin: Int = 0,
+ private val bottomHoverMargin: Int = 0,
) : OnHoverListener {
@ColorInt private var darkColor: Int = 0
@ColorInt private var lightColor: Int = 0
private var cornerRadius = 0f
+ private var leftHoverMarginInPx: Int = 0
+ private var rightHoverMarginInPx: Int = 0
+ private var topHoverMarginInPx: Int = 0
+ private var bottomHoverMarginInPx: Int = 0
private var lastTheme = HoverTheme.LIGHT
@@ -138,7 +186,12 @@ class StatusOverlayHoverListener(
val drawable =
PaintDrawable(backgroundColor).apply {
setCornerRadius(cornerRadius)
- setBounds(0, 0, v.width, v.height)
+ setBounds(
+ /*left = */ 0 + leftHoverMarginInPx,
+ /*top = */ 0 + topHoverMarginInPx,
+ /*right = */ v.width - rightHoverMarginInPx,
+ /*bottom = */ v.height - bottomHoverMarginInPx,
+ )
}
v.overlay.add(drawable)
} else if (event.action == MotionEvent.ACTION_HOVER_EXIT) {
@@ -151,5 +204,18 @@ class StatusOverlayHoverListener(
lightColor = resources.getColor(R.color.status_bar_icons_hover_color_light)
darkColor = resources.getColor(R.color.status_bar_icons_hover_color_dark)
cornerRadius = resources.getDimension(R.dimen.status_icons_hover_state_background_radius)
+ leftHoverMarginInPx = leftHoverMargin.dpToPx(resources)
+ rightHoverMarginInPx = rightHoverMargin.dpToPx(resources)
+ topHoverMarginInPx = topHoverMargin.dpToPx(resources)
+ bottomHoverMarginInPx = bottomHoverMargin.dpToPx(resources)
+ }
+
+ private fun Int.dpToPx(resources: Resources): Int {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ toFloat(),
+ resources.displayMetrics,
+ )
+ .toInt()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 03ec41d5af46..470abe63b568 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -182,7 +182,7 @@ constructor(
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
- TelephonyManager.RADIO_POWER_UNAVAILABLE
+ TelephonyManager.RADIO_POWER_UNAVAILABLE,
)
/**
@@ -265,9 +265,10 @@ constructor(
var registered = false
try {
+ logBuffer.i { "registerForCommunicationAllowedStateChanged" }
sm.registerForCommunicationAllowedStateChanged(
bgDispatcher.asExecutor(),
- callback
+ callback,
)
registered = true
} catch (e: Exception) {
@@ -276,6 +277,7 @@ constructor(
awaitClose {
if (registered) {
+ logBuffer.i { "unRegisterForCommunicationAllowedStateChanged" }
sm.unregisterForCommunicationAllowedStateChanged(callback)
}
}
@@ -321,9 +323,10 @@ constructor(
var registered = false
try {
+ logBuffer.i { "registerForSupportedStateChanged" }
satelliteManager.registerForSupportedStateChanged(
bgDispatcher.asExecutor(),
- callback
+ callback,
)
registered = true
} catch (e: Exception) {
@@ -332,6 +335,7 @@ constructor(
awaitClose {
if (registered) {
+ logBuffer.i { "unregisterForSupportedStateChanged" }
satelliteManager.unregisterForSupportedStateChanged(callback)
}
}
@@ -366,10 +370,7 @@ constructor(
var registered = false
try {
logBuffer.i { "registerForProvisionStateChanged" }
- sm.registerForProvisionStateChanged(
- bgDispatcher.asExecutor(),
- callback,
- )
+ sm.registerForProvisionStateChanged(bgDispatcher.asExecutor(), callback)
registered = true
} catch (e: Exception) {
logBuffer.e("error registering for provisioning state callback", e)
@@ -377,6 +378,7 @@ constructor(
awaitClose {
if (registered) {
+ logBuffer.i { "unregisterForProvisionStateChanged" }
sm.unregisterForProvisionStateChanged(callback)
}
}
@@ -526,17 +528,10 @@ constructor(
uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
/** A couple of convenience logging methods rather than a whole class */
- private fun LogBuffer.i(
- initializer: MessageInitializer = {},
- printer: MessagePrinter,
- ) = this.log(TAG, LogLevel.INFO, initializer, printer)
+ private fun LogBuffer.i(initializer: MessageInitializer = {}, printer: MessagePrinter) =
+ this.log(TAG, LogLevel.INFO, initializer, printer)
private fun LogBuffer.e(message: String, exception: Throwable? = null) =
- this.log(
- tag = TAG,
- level = LogLevel.ERROR,
- message = message,
- exception = exception,
- )
+ this.log(tag = TAG, level = LogLevel.ERROR, message = message, exception = exception)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
index f5cfc8c5b307..e0bf00fbe431 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
@@ -26,6 +26,7 @@ import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.vcn.VcnTransportInfo
+import android.net.vcn.VcnUtils
import android.net.wifi.WifiInfo
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.annotation.ArrayRes
@@ -161,7 +162,9 @@ constructor(
defaultNetworkCapabilities
.map { networkCapabilities ->
networkCapabilities?.run {
- val subId = (transportInfo as? VcnTransportInfo)?.subId
+ val subId =
+ VcnUtils.getSubIdFromVcnCaps(connectivityManager, networkCapabilities)
+
// Never return an INVALID_SUBSCRIPTION_ID (-1)
if (subId != INVALID_SUBSCRIPTION_ID) {
subId
@@ -245,9 +248,9 @@ constructor(
* info.
*/
fun NetworkCapabilities.getMainOrUnderlyingWifiInfo(
- connectivityManager: ConnectivityManager,
+ connectivityManager: ConnectivityManager
): WifiInfo? {
- val mainWifiInfo = this.getMainWifiInfo()
+ val mainWifiInfo = this.getMainWifiInfo(connectivityManager)
if (mainWifiInfo != null) {
return mainWifiInfo
}
@@ -264,7 +267,9 @@ constructor(
// eventually traced to a wifi or carrier merged connection. So, check those underlying
// networks for possible wifi information as well. See b/225902574.
return this.underlyingNetworks?.firstNotNullOfOrNull { underlyingNetwork ->
- connectivityManager.getNetworkCapabilities(underlyingNetwork)?.getMainWifiInfo()
+ connectivityManager
+ .getNetworkCapabilities(underlyingNetwork)
+ ?.getMainWifiInfo(connectivityManager)
}
}
@@ -272,7 +277,9 @@ constructor(
* Checks the network capabilities for wifi info, but does *not* check the underlying
* networks. See [getMainOrUnderlyingWifiInfo].
*/
- private fun NetworkCapabilities.getMainWifiInfo(): WifiInfo? {
+ private fun NetworkCapabilities.getMainWifiInfo(
+ connectivityManager: ConnectivityManager
+ ): WifiInfo? {
// Wifi info can either come from a WIFI Transport, or from a CELLULAR transport for
// virtual networks like VCN.
val canHaveWifiInfo =
@@ -286,7 +293,7 @@ constructor(
// [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of
// re-used because it makes the logic here clearer, and because the method will be
// removed once this pipeline is fully launched.
- is VcnTransportInfo -> currentTransportInfo.wifiInfo
+ is VcnTransportInfo -> VcnUtils.getWifiInfoFromVcnCaps(connectivityManager, this)
is WifiInfo -> currentTransportInfo
else -> null
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index daba1099c49d..9839f9d76537 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -16,10 +16,12 @@
package com.android.systemui.statusbar.policy.domain.interactor
+import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
import android.content.Context
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import android.service.notification.ZenPolicy.STATE_DISALLOW
import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import android.util.Log
import androidx.concurrent.futures.await
@@ -115,6 +117,26 @@ constructor(
.flowOn(bgDispatcher)
.distinctUntilChanged()
+ val activeModesBlockingEverything: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
+ mode.interruptionFilter == INTERRUPTION_FILTER_NONE
+ }
+
+ val activeModesBlockingMedia: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
+ mode.policy.priorityCategoryMedia == STATE_DISALLOW
+ }
+
+ val activeModesBlockingAlarms: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
+ mode.policy.priorityCategoryAlarms == STATE_DISALLOW
+ }
+
+ private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> {
+ return modes
+ .map { modes -> modes.filter { mode -> predicate(mode) } }
+ .map { modes -> buildActiveZenModes(modes) }
+ .flowOn(bgDispatcher)
+ .distinctUntilChanged()
+ }
+
suspend fun getActiveModes() = buildActiveZenModes(zenModeRepository.getModes())
private suspend fun buildActiveZenModes(modes: List<ZenMode>): ActiveZenModes {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
index 421e5c45bbfe..e8dc93465685 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -16,8 +16,10 @@
package com.android.systemui.statusbar.window
+import android.content.Context
import android.view.View
import android.view.ViewGroup
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.fragments.FragmentHostManager
import java.util.Optional
@@ -73,4 +75,11 @@ interface StatusBarWindowController {
* this#setForceStatusBarVisible} together and use some sort of ranking system instead.
*/
fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean)
+
+ interface Factory {
+ fun create(
+ context: Context,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
+ ): StatusBarWindowController
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 1ee7cf3490f4..d709e5a0cd6c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -354,11 +354,13 @@ public class StatusBarWindowControllerImpl implements StatusBarWindowController
}
@AssistedFactory
- public interface Factory {
+ public interface Factory extends StatusBarWindowController.Factory {
/** Creates a new instance. */
+ @NonNull
+ @Override
StatusBarWindowControllerImpl create(
- Context context,
- ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
+ @NonNull Context context,
+ @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
new file mode 100644
index 000000000000..5f30b3719aa7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.view.Display
+import android.view.WindowManager
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Store that allows to retrieve per display instances of [StatusBarWindowController]. */
+interface StatusBarWindowControllerStore {
+ /**
+ * The instance for the default/main display of the device. For example, on a phone or a tablet,
+ * the default display is the internal/built-in display of the device.
+ *
+ * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
+ */
+ val defaultDisplay: StatusBarWindowController
+
+ /**
+ * Returns an instance for a specific display id.
+ *
+ * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
+ * displays.
+ */
+ fun forDisplay(displayId: Int): StatusBarWindowController
+}
+
+@SysUISingleton
+class MultiDisplayStatusBarWindowControllerStore
+@Inject
+constructor(
+ @Background private val backgroundApplicationScope: CoroutineScope,
+ private val controllerFactory: StatusBarWindowController.Factory,
+ private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
+ private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory,
+ private val displayRepository: DisplayRepository,
+) : StatusBarWindowControllerStore, CoreStartable {
+
+ init {
+ StatusBarConnectedDisplays.assertInNewMode()
+ }
+
+ private val perDisplayControllers = ConcurrentHashMap<Int, StatusBarWindowController>()
+
+ override fun start() {
+ backgroundApplicationScope.launch(CoroutineName("StatusBarWindowController#start")) {
+ displayRepository.displayRemovalEvent.collect { displayId ->
+ perDisplayControllers.remove(displayId)
+ }
+ }
+ }
+
+ override val defaultDisplay: StatusBarWindowController
+ get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+ override fun forDisplay(displayId: Int): StatusBarWindowController {
+ if (displayRepository.getDisplay(displayId) == null) {
+ throw IllegalArgumentException("Display with id $displayId doesn't exist.")
+ }
+ return perDisplayControllers.computeIfAbsent(displayId) {
+ createControllerForDisplay(displayId)
+ }
+ }
+
+ private fun createControllerForDisplay(displayId: Int): StatusBarWindowController {
+ val statusBarDisplayContext =
+ displayWindowPropertiesRepository.get(
+ displayId = displayId,
+ windowType = WindowManager.LayoutParams.TYPE_STATUS_BAR,
+ )
+ val viewCaptureAwareWindowManager =
+ viewCaptureAwareWindowManagerFactory.create(statusBarDisplayContext.windowManager)
+ return controllerFactory.create(
+ statusBarDisplayContext.context,
+ viewCaptureAwareWindowManager,
+ )
+ }
+}
+
+@SysUISingleton
+class SingleDisplayStatusBarWindowControllerStore
+@Inject
+constructor(private val controller: StatusBarWindowController) : StatusBarWindowControllerStore {
+
+ init {
+ StatusBarConnectedDisplays.assertInLegacyMode()
+ }
+
+ override val defaultDisplay = controller
+
+ override fun forDisplay(displayId: Int) = controller
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 411ff8b2b542..6879a3415238 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -16,6 +16,7 @@
package com.android.systemui.touchpad.tutorial.ui.composable
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
@@ -26,10 +27,7 @@ import com.android.systemui.res.R
import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor
@Composable
-fun BackGestureTutorialScreen(
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
+fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -38,18 +36,20 @@ fun BackGestureTutorialScreen(
titleResId = R.string.touchpad_back_gesture_action_title,
bodyResId = R.string.touchpad_back_gesture_guidance,
titleSuccessResId = R.string.touchpad_back_gesture_success_title,
- bodySuccessResId = R.string.touchpad_back_gesture_success_body
+ bodySuccessResId = R.string.touchpad_back_gesture_success_body,
),
animations =
TutorialScreenConfig.Animations(
educationResId = R.raw.trackpad_back_edu,
- successResId = R.raw.trackpad_back_success
- )
+ successResId = R.raw.trackpad_back_success,
+ ),
)
val gestureMonitorProvider =
DistanceBasedGestureMonitorProvider(
monitorFactory = { distanceThresholdPx, gestureStateCallback ->
- BackGestureMonitor(distanceThresholdPx, gestureStateCallback)
+ BackGestureMonitor(distanceThresholdPx).also {
+ it.addGestureStateCallback(gestureStateCallback)
+ }
}
)
GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -57,7 +57,7 @@ fun BackGestureTutorialScreen(
@Composable
private fun rememberScreenColors(): TutorialScreenConfig.Colors {
- val onTertiary = LocalAndroidColorScheme.current.onTertiary
+ val onTertiary = MaterialTheme.colorScheme.onTertiary
val onTertiaryFixed = LocalAndroidColorScheme.current.onTertiaryFixed
val onTertiaryFixedVariant = LocalAndroidColorScheme.current.onTertiaryFixedVariant
val tertiaryFixedDim = LocalAndroidColorScheme.current.tertiaryFixedDim
@@ -66,7 +66,7 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors {
rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim),
rememberColorFilterProperty(".onTertiaryFixed", onTertiaryFixed),
rememberColorFilterProperty(".onTertiary", onTertiary),
- rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant)
+ rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant),
)
val screenColors =
remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index f2fec5f5d9b1..a55fa442cd96 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -26,10 +26,7 @@ import com.android.systemui.res.R
import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor
@Composable
-fun HomeGestureTutorialScreen(
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
+fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -38,18 +35,20 @@ fun HomeGestureTutorialScreen(
titleResId = R.string.touchpad_home_gesture_action_title,
bodyResId = R.string.touchpad_home_gesture_guidance,
titleSuccessResId = R.string.touchpad_home_gesture_success_title,
- bodySuccessResId = R.string.touchpad_home_gesture_success_body
+ bodySuccessResId = R.string.touchpad_home_gesture_success_body,
),
animations =
TutorialScreenConfig.Animations(
educationResId = R.raw.trackpad_home_edu,
- successResId = R.raw.trackpad_home_success
- )
+ successResId = R.raw.trackpad_home_success,
+ ),
)
val gestureMonitorProvider =
DistanceBasedGestureMonitorProvider(
monitorFactory = { distanceThresholdPx, gestureStateCallback ->
- HomeGestureMonitor(distanceThresholdPx, gestureStateCallback)
+ HomeGestureMonitor(distanceThresholdPx).also {
+ it.addGestureStateCallback(gestureStateCallback)
+ }
}
)
GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -64,7 +63,7 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors {
rememberLottieDynamicProperties(
rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
rememberColorFilterProperty(".onPrimaryFixed", onPrimaryFixed),
- rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant)
+ rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant),
)
val screenColors =
remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index b2fb6cdfcee5..6ee15aa952f4 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -29,10 +29,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureMonito
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
@Composable
-fun RecentAppsGestureTutorialScreen(
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
+fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -41,20 +38,20 @@ fun RecentAppsGestureTutorialScreen(
titleResId = R.string.touchpad_recent_apps_gesture_action_title,
bodyResId = R.string.touchpad_recent_apps_gesture_guidance,
titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title,
- bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body
+ bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body,
),
animations =
TutorialScreenConfig.Animations(
educationResId = R.raw.trackpad_recent_apps_edu,
- successResId = R.raw.trackpad_recent_apps_success
- )
+ successResId = R.raw.trackpad_recent_apps_success,
+ ),
)
val gestureMonitorProvider =
object : GestureMonitorProvider {
@Composable
override fun rememberGestureMonitor(
resources: Resources,
- gestureStateChangedCallback: (GestureState) -> Unit
+ gestureStateChangedCallback: (GestureState) -> Unit,
): TouchpadGestureMonitor {
val distanceThresholdPx =
resources.getDimensionPixelSize(
@@ -63,11 +60,9 @@ fun RecentAppsGestureTutorialScreen(
val velocityThresholdPxPerMs =
resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold)
return remember(distanceThresholdPx, velocityThresholdPxPerMs) {
- RecentAppsGestureMonitor(
- distanceThresholdPx,
- gestureStateChangedCallback,
- velocityThresholdPxPerMs
- )
+ RecentAppsGestureMonitor(distanceThresholdPx, velocityThresholdPxPerMs).also {
+ it.addGestureStateCallback(gestureStateChangedCallback)
+ }
}
}
}
@@ -83,7 +78,7 @@ private fun rememberScreenColors(): TutorialScreenConfig.Colors {
rememberLottieDynamicProperties(
rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim),
rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed),
- rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
+ rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant),
)
val screenColors =
remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
index 94e19deb0006..3c31efa6265a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
@@ -84,7 +84,7 @@ private fun TutorialSelectionButtons(
) {
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_home_gesture_button),
- icon = Icons.AutoMirrored.Outlined.ArrowBack,
+ icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
iconColor = MaterialTheme.colorScheme.onPrimary,
onClick = onHomeTutorialClicked,
backgroundColor = MaterialTheme.colorScheme.primary,
@@ -92,7 +92,7 @@ private fun TutorialSelectionButtons(
)
TutorialButton(
text = stringResource(R.string.touchpad_tutorial_back_gesture_button),
- icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
+ icon = Icons.AutoMirrored.Outlined.ArrowBack,
iconColor = MaterialTheme.colorScheme.onTertiary,
onClick = onBackTutorialClicked,
backgroundColor = MaterialTheme.colorScheme.tertiary,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index 084da2c59776..490f04d55802 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -16,26 +16,27 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
+import android.view.MotionEvent
import kotlin.math.abs
/** Monitors for touchpad back gesture, that is three fingers swiping left or right */
-class BackGestureMonitor(
- override val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit
-) :
- TouchpadGestureMonitor by ThreeFingerDistanceBasedGestureMonitor(
- gestureDistanceThresholdPx = gestureDistanceThresholdPx,
- gestureStateChangedCallback = gestureStateChangedCallback,
- donePredicate =
- object : GestureDonePredicate {
- override fun wasGestureDone(
- startX: Float,
- startY: Float,
- endX: Float,
- endY: Float
- ): Boolean {
- val distance = abs(endX - startX)
- return distance >= gestureDistanceThresholdPx
- }
- }
- )
+class BackGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
+
+ private val distanceTracker = DistanceTracker()
+ private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+ override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+ gestureStateChangedCallback = callback
+ }
+
+ override fun accept(event: MotionEvent) {
+ if (!isThreeFingerTouchpadSwipe(event)) return
+ val gestureState = distanceTracker.processEvent(event)
+ updateGestureState(
+ gestureStateChangedCallback,
+ gestureState,
+ isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
+ progress = { 0f },
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
new file mode 100644
index 000000000000..d48235892d69
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+
+/**
+ * Tracks distance change for processed MotionEvents. Useful for recognizing gestures based on
+ * distance travelled instead of specific position on the screen.
+ */
+class DistanceTracker(var startX: Float = 0f, var startY: Float = 0f) {
+ fun processEvent(event: MotionEvent): DistanceGestureState? {
+ val action = event.actionMasked
+ return when (action) {
+ MotionEvent.ACTION_DOWN -> {
+ startX = event.x
+ startY = event.y
+ Started(event.x, event.y)
+ }
+ MotionEvent.ACTION_MOVE -> Moving(event.x - startX, event.y - startY)
+ MotionEvent.ACTION_UP -> Finished(event.x - startX, event.y - startY)
+ else -> null
+ }
+ }
+}
+
+sealed class DistanceGestureState(val deltaX: Float, val deltaY: Float)
+
+class Started(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
+
+class Moving(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
+
+class Finished(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
new file mode 100644
index 000000000000..f19467726def
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+/** Helper function for gesture recognizers to have common state triggering logic */
+inline fun updateGestureState(
+ gestureStateChangedCallback: (GestureState) -> Unit,
+ gestureState: DistanceGestureState?,
+ isFinished: (Finished) -> Boolean,
+ progress: (Moving) -> Float,
+) {
+ when (gestureState) {
+ is Finished -> {
+ if (isFinished(gestureState)) {
+ gestureStateChangedCallback(GestureState.Finished)
+ } else {
+ gestureStateChangedCallback(GestureState.NotStarted)
+ }
+ }
+ is Moving -> {
+ gestureStateChangedCallback(GestureState.InProgress(progress(gestureState)))
+ }
+ is Started -> gestureStateChangedCallback(GestureState.InProgress())
+ else -> {}
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
index a9aa5c897573..83d4f566257b 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
@@ -16,24 +16,26 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
+import android.view.MotionEvent
+
/** Monitors for touchpad home gesture, that is three fingers swiping up */
-class HomeGestureMonitor(
- override val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit
-) :
- TouchpadGestureMonitor by ThreeFingerDistanceBasedGestureMonitor(
- gestureDistanceThresholdPx = gestureDistanceThresholdPx,
- gestureStateChangedCallback = gestureStateChangedCallback,
- donePredicate =
- object : GestureDonePredicate {
- override fun wasGestureDone(
- startX: Float,
- startY: Float,
- endX: Float,
- endY: Float
- ): Boolean {
- val distance = startY - endY
- return distance >= gestureDistanceThresholdPx
- }
- }
- )
+class HomeGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
+
+ private val distanceTracker = DistanceTracker()
+ private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+ override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+ gestureStateChangedCallback = callback
+ }
+
+ override fun accept(event: MotionEvent) {
+ if (!isThreeFingerTouchpadSwipe(event)) return
+ val gestureState = distanceTracker.processEvent(event)
+ updateGestureState(
+ gestureStateChangedCallback,
+ gestureState,
+ isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
+ progress = { 0f },
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
index ca3880a5dfe6..1731bb85fba4 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
@@ -17,7 +17,6 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.MotionEvent
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D
import kotlin.math.abs
/**
@@ -26,46 +25,31 @@ import kotlin.math.abs
* is based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker]
*/
class RecentAppsGestureMonitor(
- override val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit,
+ private val gestureDistanceThresholdPx: Int,
private val velocityThresholdPxPerMs: Float,
- private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false),
+ private val distanceTracker: DistanceTracker = DistanceTracker(),
+ private val velocityTracker: VerticalVelocityTracker = VerticalVelocityTracker(),
) : TouchpadGestureMonitor {
- private var xStart = 0f
- private var yStart = 0f
+ private var gestureStateChangedCallback: (GestureState) -> Unit = {}
- override fun processTouchpadEvent(event: MotionEvent) {
- val action = event.actionMasked
- velocityTracker.addDataPoint(event.eventTime, event.y)
- when (action) {
- MotionEvent.ACTION_DOWN -> {
- if (isThreeFingerTouchpadSwipe(event)) {
- xStart = event.x
- yStart = event.y
- gestureStateChangedCallback(GestureState.InProgress())
- }
- }
- MotionEvent.ACTION_UP -> {
- if (isThreeFingerTouchpadSwipe(event) && isRecentAppsGesture(event)) {
- gestureStateChangedCallback(GestureState.Finished)
- } else {
- gestureStateChangedCallback(GestureState.NotStarted)
- }
- velocityTracker.resetTracking()
- }
- MotionEvent.ACTION_CANCEL -> {
- velocityTracker.resetTracking()
- }
- }
+ override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+ gestureStateChangedCallback = callback
}
- private fun isRecentAppsGesture(event: MotionEvent): Boolean {
- // below is trying to mirror behavior of TriggerSwipeUpTouchTracker#onGestureEnd.
- // We're diving velocity by 1000, to have the same unit of measure: pixels/ms.
- val swipeDistance = yStart - event.y
- val velocity = velocityTracker.calculateVelocity() / 1000
- return swipeDistance >= gestureDistanceThresholdPx &&
- abs(velocity) <= velocityThresholdPxPerMs
+ override fun accept(event: MotionEvent) {
+ if (!isThreeFingerTouchpadSwipe(event)) return
+ val gestureState = distanceTracker.processEvent(event)
+ velocityTracker.accept(event)
+
+ updateGestureState(
+ gestureStateChangedCallback,
+ gestureState,
+ isFinished = { state ->
+ -state.deltaY >= gestureDistanceThresholdPx &&
+ abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs
+ },
+ progress = { 0f },
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerDistanceBasedGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerDistanceBasedGestureMonitor.kt
deleted file mode 100644
index 12bcaeac04dc..000000000000
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/ThreeFingerDistanceBasedGestureMonitor.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.touchpad.tutorial.ui.gesture
-
-import android.view.MotionEvent
-
-interface GestureDonePredicate {
- /**
- * Should return if gesture was finished. The only events this predicate receives are ACTION_UP.
- */
- fun wasGestureDone(startX: Float, startY: Float, endX: Float, endY: Float): Boolean
-}
-
-/**
- * Common implementation for three-finger gesture monitors that are only distance-based. E.g. recent
- * apps gesture is not only distance-based because it requires going over threshold distance and
- * slowing down the movement.
- */
-class ThreeFingerDistanceBasedGestureMonitor(
- override val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit,
- private val donePredicate: GestureDonePredicate
-) : TouchpadGestureMonitor {
-
- private var xStart = 0f
- private var yStart = 0f
-
- override fun processTouchpadEvent(event: MotionEvent) {
- val action = event.actionMasked
- when (action) {
- MotionEvent.ACTION_DOWN -> {
- if (isThreeFingerTouchpadSwipe(event)) {
- xStart = event.x
- yStart = event.y
- gestureStateChangedCallback(GestureState.InProgress())
- }
- }
- MotionEvent.ACTION_UP -> {
- if (isThreeFingerTouchpadSwipe(event)) {
- if (donePredicate.wasGestureDone(xStart, yStart, event.x, event.y)) {
- gestureStateChangedCallback(GestureState.Finished)
- } else {
- gestureStateChangedCallback(GestureState.NotStarted)
- }
- }
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index 88671d41f1cd..4b82ba1c0dda 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -18,13 +18,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.InputDevice
import android.view.MotionEvent
+import java.util.function.Consumer
/**
* Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all
* motion events passed to [onMotionEvent] and will filter touchpad events accordingly
*/
class TouchpadGestureHandler(
- private val gestureMonitor: TouchpadGestureMonitor,
+ private val gestureMonitor: Consumer<MotionEvent>,
private val easterEggGestureMonitor: EasterEggGestureMonitor,
) {
@@ -40,7 +41,7 @@ class TouchpadGestureHandler(
if (isTwoFingerSwipe(event)) {
easterEggGestureMonitor.processTouchpadEvent(event)
} else {
- gestureMonitor.processTouchpadEvent(event)
+ gestureMonitor.accept(event)
}
true
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
index 8774a9286022..9216821272ff 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
@@ -17,17 +17,11 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.MotionEvent
+import java.util.function.Consumer
-/**
- * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
- * changes. All tracked motion events should be passed to [processTouchpadEvent]
- */
-interface TouchpadGestureMonitor {
-
- val gestureDistanceThresholdPx: Int
- val gestureStateChangedCallback: (GestureState) -> Unit
-
- fun processTouchpadEvent(event: MotionEvent)
+/** Monitor for touchpad gestures that can notify callback when [GestureState] changes. */
+interface TouchpadGestureMonitor : Consumer<MotionEvent> {
+ fun addGestureStateCallback(callback: (GestureState) -> Unit)
}
fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
new file mode 100644
index 000000000000..9b38eca89f15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+import androidx.compose.ui.input.pointer.util.VelocityTracker1D
+import java.util.function.Consumer
+
+/** Velocity in pixels/ms. */
+@JvmInline value class Velocity(val value: Float)
+
+/**
+ * Tracks velocity for processed MotionEvents. Useful for recognizing gestures based on velocity.
+ */
+interface VelocityTracker : Consumer<MotionEvent> {
+
+ fun calculateVelocity(): Velocity
+}
+
+class VerticalVelocityTracker(
+ private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false)
+) : VelocityTracker {
+
+ override fun accept(event: MotionEvent) {
+ val action = event.actionMasked
+ if (action == MotionEvent.ACTION_DOWN) {
+ velocityTracker.resetTracking()
+ }
+ velocityTracker.addDataPoint(event.eventTime, event.y)
+ }
+
+ /**
+ * Calculates velocity on demand - this calculation can be expensive so shouldn't be called
+ * after every event.
+ */
+ override fun calculateVelocity() = Velocity(velocityTracker.calculateVelocity() / 1000)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 1f92bc1df9c8..bbd8f3dc7e82 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -59,7 +59,6 @@ import android.view.accessibility.CaptioningManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Observer;
import com.android.internal.annotations.GuardedBy;
@@ -110,8 +109,8 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa
// It is safe to use 99 as the broadcast stream now. There are only 10+ default audio
// streams defined in AudioSystem for now and audio team is in the middle of restructure,
// no new default stream is preferred.
- @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99;
- private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
+ public static final int DYNAMIC_STREAM_BROADCAST = 99;
+ public static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES =
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 7166428d863f..7c5116dbf72c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -537,7 +537,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable,
mWindow.setAttributes(lp);
mWindow.setLayout(WRAP_CONTENT, WRAP_CONTENT);
- mDialog.setContentView(R.layout.volume_dialog);
+ mDialog.setContentView(R.layout.volume_dialog_legacy);
mDialogView = mDialog.findViewById(R.id.volume_dialog);
mDialogView.setAlpha(0);
mDialogTimeoutMillis = mSecureSettings.get().getInt(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
index 3fdf86a923fb..cd8cdc8573bd 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
@@ -17,6 +17,13 @@
package com.android.systemui.volume.dialog.dagger.module
import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.utils.VolumeTracer
+import com.android.systemui.volume.dialog.utils.VolumeTracerImpl
+import dagger.Binds
import dagger.Module
-@Module(subcomponents = [VolumeDialogComponent::class]) interface VolumeDialogPluginModule
+@Module(subcomponents = [VolumeDialogComponent::class])
+interface VolumeDialogPluginModule {
+
+ @Binds fun bindVolumeTracer(volumeTracer: VolumeTracerImpl): VolumeTracer
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt
new file mode 100644
index 000000000000..2aeaa5cd7248
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/data/VolumeDialogVisibilityRepository.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.data
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+@SysUISingleton
+class VolumeDialogVisibilityRepository @Inject constructor() {
+
+ private val mutableDialogVisibility =
+ MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
+ val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+
+ fun updateVisibility(
+ update: (current: VolumeDialogVisibilityModel) -> VolumeDialogVisibilityModel
+ ) {
+ mutableDialogVisibility.update(update)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
index f7d6d90ef6f0..2668589be1b3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
@@ -20,8 +20,12 @@ import android.annotation.SuppressLint
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
-import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Dismissed
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel.Visible
+import com.android.systemui.volume.dialog.utils.VolumeTracer
import javax.inject.Inject
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -30,13 +34,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.update
private val MAX_DIALOG_SHOW_TIME: Duration = 3.seconds
@@ -53,14 +55,13 @@ class VolumeDialogVisibilityInteractor
constructor(
@VolumeDialogPlugin coroutineScope: CoroutineScope,
callbacksInteractor: VolumeDialogCallbacksInteractor,
+ private val tracer: VolumeTracer,
+ private val repository: VolumeDialogVisibilityRepository,
) {
@SuppressLint("SharedFlowCreation")
private val mutableDismissDialogEvents = MutableSharedFlow<Unit>()
- private val mutableDialogVisibility =
- MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
-
- val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+ val dialogVisibility: Flow<VolumeDialogVisibilityModel> = repository.dialogVisibility
init {
merge(
@@ -70,12 +71,11 @@ constructor(
},
callbacksInteractor.event,
)
- .onEach { event ->
- VolumeDialogVisibilityModel.fromEvent(event)?.let { model ->
- mutableDialogVisibility.value = model
- if (model is VolumeDialogVisibilityModel.Visible) {
- resetDismissTimeout()
- }
+ .mapNotNull { it.toVisibilityModel() }
+ .onEach { model ->
+ updateVisibility { model }
+ if (model is VolumeDialogVisibilityModel.Visible) {
+ resetDismissTimeout()
}
}
.launchIn(coroutineScope)
@@ -86,9 +86,9 @@ constructor(
* [dialogVisibility].
*/
fun dismissDialog(reason: Int) {
- mutableDialogVisibility.update {
- if (it is VolumeDialogVisibilityModel.Dismissed) {
- it
+ updateVisibility { visibilityModel ->
+ if (visibilityModel is VolumeDialogVisibilityModel.Dismissed) {
+ visibilityModel
} else {
VolumeDialogVisibilityModel.Dismissed(reason)
}
@@ -99,4 +99,19 @@ constructor(
suspend fun resetDismissTimeout() {
mutableDismissDialogEvents.emit(Unit)
}
+
+ private fun updateVisibility(
+ update: (VolumeDialogVisibilityModel) -> VolumeDialogVisibilityModel
+ ) {
+ repository.updateVisibility { update(it).also(tracer::traceVisibilityStart) }
+ }
+
+ private fun VolumeDialogEventModel.toVisibilityModel(): VolumeDialogVisibilityModel? {
+ return when (this) {
+ is VolumeDialogEventModel.DismissRequested -> Dismissed(reason)
+ is VolumeDialogEventModel.ShowRequested ->
+ Visible(reason, keyguardLocked, lockTaskModeState)
+ else -> null
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
index f1443e36d019..500cc0bf748c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
@@ -23,7 +23,7 @@ import com.android.systemui.plugins.VolumeDialogController
/** Models a state of the Volume Dialog. */
data class VolumeDialogStateModel(
- val states: Map<Int, VolumeDialogStreamStateModel>,
+ val states: Map<Int, VolumeDialogStreamModel>,
val ringerModeInternal: Int = 0,
val ringerModeExternal: Int = 0,
val zenMode: Int = 0,
@@ -39,7 +39,7 @@ data class VolumeDialogStateModel(
constructor(
legacyState: VolumeDialogController.State
) : this(
- states = legacyState.states.mapToMap { VolumeDialogStreamStateModel(it) },
+ states = legacyState.states.mapToMap { VolumeDialogStreamModel(it) },
ringerModeInternal = legacyState.ringerModeInternal,
ringerModeExternal = legacyState.ringerModeExternal,
zenMode = legacyState.zenMode,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
index a9d367da41e4..26c96eabc65f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
@@ -16,18 +16,18 @@
package com.android.systemui.volume.dialog.domain.model
-import android.annotation.IntegerRes
+import androidx.annotation.StringRes
import com.android.systemui.plugins.VolumeDialogController
/** Models a state of an audio stream of the Volume Dialog. */
-data class VolumeDialogStreamStateModel(
+data class VolumeDialogStreamModel(
val isDynamic: Boolean = false,
val level: Int = 0,
val levelMin: Int = 0,
val levelMax: Int = 0,
val muted: Boolean = false,
val muteSupported: Boolean = false,
- @IntegerRes val name: Int = 0,
+ @StringRes val name: Int = 0,
val remoteLabel: String? = null,
val routedToBluetooth: Boolean = false,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt
index db196347d4a9..2dd0bdab93d1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractor.kt
@@ -23,7 +23,7 @@ import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
-import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
index ba08876609ae..b2f6cb332e74 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
@@ -34,7 +34,7 @@ constructor(private val viewModelFactory: VolumeDialogSettingsButtonViewModel.Fa
fun bind(view: View) {
with(view) {
- val button = requireViewById<View>(R.id.settings)
+ val button = requireViewById<View>(R.id.volume_dialog_settings)
repeatWhenAttached {
viewModel(
traceName = "VolumeDialogViewBinder",
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogVisibilityModel.kt
index 646445d33f51..56a707d86598 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogVisibilityModel.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.volume.dialog.domain.model
+package com.android.systemui.volume.dialog.shared.model
/** Models current Volume Dialog visibility state. */
sealed interface VolumeDialogVisibilityModel {
@@ -30,19 +30,4 @@ sealed interface VolumeDialogVisibilityModel {
/** Dialog has been shown and then dismissed. */
data class Dismissed(val reason: Int) : Invisible
-
- companion object {
-
- /**
- * Creates [VolumeDialogVisibilityModel] from appropriate events and returns null otherwise.
- */
- fun fromEvent(event: VolumeDialogEventModel): VolumeDialogVisibilityModel? {
- return when (event) {
- is VolumeDialogEventModel.DismissRequested -> Dismissed(event.reason)
- is VolumeDialogEventModel.ShowRequested ->
- Visible(event.reason, event.keyguardLocked, event.lockTaskModeState)
- else -> null
- }
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
new file mode 100644
index 000000000000..81507ba7dc60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.domain.interactor
+
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
+
+/** Operates a state of particular slider of the Volume Dialog. */
+class VolumeDialogSliderInteractor
+@AssistedInject
+constructor(
+ @Assisted private val sliderType: VolumeDialogSliderType,
+ volumeDialogStateInteractor: VolumeDialogStateInteractor,
+ private val volumeDialogController: VolumeDialogController,
+) {
+
+ val slider: Flow<VolumeDialogStreamModel> =
+ volumeDialogStateInteractor.volumeDialogState.mapNotNull {
+ it.states[sliderType.audioStream]
+ }
+
+ fun setStreamVolume(userLevel: Int) {
+ volumeDialogController.setStreamVolume(sliderType.audioStream, userLevel)
+ }
+
+ @VolumeDialogScope
+ @AssistedFactory
+ interface Factory {
+
+ fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderInteractor
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
new file mode 100644
index 000000000000..325e4c9514cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.domain.interactor
+
+import com.android.systemui.volume.VolumeDialogControllerImpl
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSlidersModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Provides a state for the Sliders section of the Volume Dialog. */
+@VolumeDialogScope
+class VolumeDialogSlidersInteractor
+@Inject
+constructor(volumeDialogStateInteractor: VolumeDialogStateInteractor) {
+
+ val sliders: Flow<VolumeDialogSlidersModel> =
+ volumeDialogStateInteractor.volumeDialogState.map {
+ val sliderTypes: List<VolumeDialogSliderType> =
+ it.states.keys.sortedWith(StreamsSorter).map { audioStream ->
+ when {
+ audioStream == VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST ->
+ VolumeDialogSliderType.AudioSharingStream(audioStream)
+ audioStream >=
+ VolumeDialogControllerImpl.DYNAMIC_STREAM_REMOTE_START_INDEX ->
+ VolumeDialogSliderType.RemoteMediaStream(audioStream)
+ else -> VolumeDialogSliderType.Stream(audioStream)
+ }
+ }
+ VolumeDialogSlidersModel(
+ slider = sliderTypes.first(),
+ floatingSliders = sliderTypes.drop(1),
+ )
+ }
+
+ private object StreamsSorter : Comparator<Int> {
+
+ // TODO(b/369992924) order the streams
+ override fun compare(lhs: Int, rhs: Int): Int {
+ return lhs - rhs
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt
new file mode 100644
index 000000000000..18a26891e904
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.domain.model
+
+/** Models different possible audio sliders shown in the Volume Dialog. */
+sealed interface VolumeDialogSliderType {
+
+ // VolumeDialogController uses the same model for every slider type. We need to follow the same
+ // logic until we refactor and decouple data and domain layers from the VolumeDialogController
+ // into separated interactors.
+ val audioStream: Int
+
+ class Stream(override val audioStream: Int) : VolumeDialogSliderType
+
+ class RemoteMediaStream(override val audioStream: Int) : VolumeDialogSliderType
+
+ class AudioSharingStream(override val audioStream: Int) : VolumeDialogSliderType
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt
new file mode 100644
index 000000000000..91a332830b75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.domain.model
+
+/** Models a state of the sliders section of the Volume Dialog. */
+data class VolumeDialogSlidersModel(
+ val slider: VolumeDialogSliderType,
+ val floatingSliders: List<VolumeDialogSliderType>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
new file mode 100644
index 000000000000..25a5f287c21f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import android.view.View
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+
+class VolumeDialogSliderViewBinder
+@AssistedInject
+constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderViewModel) {
+
+ fun bind(view: View) {
+ with(view) {
+ repeatWhenAttached {
+ viewModel(
+ traceName = "VolumeDialogSliderViewBinder",
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { viewModelProvider() },
+ ) { viewModel ->
+ setSnapshotBinding {}
+
+ awaitCancellation()
+ }
+ }
+ }
+ }
+
+ @AssistedFactory
+ @VolumeDialogScope
+ interface Factory {
+
+ fun create(
+ viewModelProvider: () -> VolumeDialogSliderViewModel
+ ): VolumeDialogSliderViewBinder
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
new file mode 100644
index 000000000000..0a00f70b54f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui
+
+import android.view.View
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.awaitCancellation
+
+@VolumeDialogScope
+class VolumeDialogSlidersViewBinder
+@Inject
+constructor(private val viewModelFactory: VolumeDialogSlidersViewModel.Factory) {
+
+ fun bind(view: View) {
+ with(view) {
+ repeatWhenAttached {
+ viewModel(
+ traceName = "VolumeDialogSlidersViewBinder",
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { viewModelFactory.create() },
+ ) { viewModel ->
+ setSnapshotBinding {}
+
+ awaitCancellation()
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
new file mode 100644
index 000000000000..27b8f2f5adb7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+
+class VolumeDialogSliderViewModel
+@AssistedInject
+constructor(@Assisted private val interactor: VolumeDialogSliderInteractor) {
+
+ val model: Flow<VolumeDialogStreamModel> = interactor.slider
+
+ fun setStreamVolume(volume: Int) {
+ interactor.setStreamVolume(volume)
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(interactor: VolumeDialogSliderInteractor): VolumeDialogSliderViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt
new file mode 100644
index 000000000000..b5b292fa4a66
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSlidersInteractor
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class VolumeDialogSlidersViewModel
+@AssistedInject
+constructor(
+ @VolumeDialog coroutineScope: CoroutineScope,
+ private val slidersInteractor: VolumeDialogSlidersInteractor,
+ private val sliderInteractorFactory: VolumeDialogSliderInteractor.Factory,
+ private val sliderViewModelFactory: VolumeDialogSliderViewModel.Factory,
+ private val sliderViewBinderFactory: VolumeDialogSliderViewBinder.Factory,
+) {
+
+ val sliders: Flow<VolumeDialogSliderUiModel> =
+ slidersInteractor.sliders
+ .distinctUntilChanged()
+ .map { slidersModel ->
+ VolumeDialogSliderUiModel(
+ sliderViewBinder = createSliderViewBinder(slidersModel.slider),
+ floatingSliderViewBinders =
+ slidersModel.floatingSliders.map(::createSliderViewBinder),
+ )
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+ .filterNotNull()
+
+ private fun createSliderViewBinder(type: VolumeDialogSliderType): VolumeDialogSliderViewBinder =
+ sliderViewBinderFactory.create {
+ sliderViewModelFactory.create(sliderInteractorFactory.create(type))
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(): VolumeDialogSlidersViewModel
+ }
+}
+
+/** Models slider ui */
+data class VolumeDialogSliderUiModel(
+ val sliderViewBinder: VolumeDialogSliderViewBinder,
+ val floatingSliderViewBinders: List<VolumeDialogSliderViewBinder>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
index 9c88303106ae..77733fe33275 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
@@ -33,6 +33,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+/** Binds the Volume Dialog itself. */
@VolumeDialogScope
class VolumeDialogBinder
@Inject
@@ -47,9 +48,13 @@ constructor(
with(dialog) {
setupWindow(window!!)
dialog.setContentView(R.layout.volume_dialog)
+ dialog.setCanceledOnTouchOutside(true)
- settingsButtonViewBinder.bind(dialog.requireViewById(R.id.settings_container))
- volumeDialogViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_container))
+ settingsButtonViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_settings))
+ volumeDialogViewBinder.bind(
+ dialog,
+ dialog.requireViewById(R.id.volume_dialog_container),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
index 600d17603964..23e6eac05ea8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -16,32 +16,144 @@
package com.android.systemui.volume.dialog.ui.binder
+import android.app.Dialog
+import android.view.Gravity
import android.view.View
+import com.android.internal.view.RotationPolicy
import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.lifecycle.setSnapshotBinding
import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.SystemUIInterpolators
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory
+import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogResourcesViewModel
import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel
+import com.android.systemui.volume.dialog.utils.VolumeTracer
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+/** Binds the root view of the Volume Dialog. */
+@OptIn(ExperimentalCoroutinesApi::class)
@VolumeDialogScope
class VolumeDialogViewBinder
@Inject
-constructor(private val volumeDialogViewModelFactory: VolumeDialogViewModel.Factory) {
+constructor(
+ private val volumeResources: VolumeDialogResourcesViewModel,
+ private val gravityViewModel: VolumeDialogGravityViewModel,
+ private val viewModelFactory: VolumeDialogViewModel.Factory,
+ private val jankListenerFactory: JankListenerFactory,
+ private val tracer: VolumeTracer,
+) {
- fun bind(view: View) {
+ fun bind(dialog: Dialog, view: View) {
+ view.alpha = 0f
view.repeatWhenAttached {
view.viewModel(
traceName = "VolumeDialogViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { volumeDialogViewModelFactory.create() },
+ factory = { viewModelFactory.create() },
) { viewModel ->
- view.setSnapshotBinding {}
+ animateVisibility(view, dialog, viewModel.dialogVisibilityModel)
awaitCancellation()
}
}
}
+
+ private fun CoroutineScope.animateVisibility(
+ view: View,
+ dialog: Dialog,
+ visibilityModel: Flow<VolumeDialogVisibilityModel>,
+ ) {
+ visibilityModel
+ .mapLatest {
+ when (it) {
+ is VolumeDialogVisibilityModel.Visible -> {
+ tracer.traceVisibilityEnd(it)
+ calculateTranslationX(view)?.let(view::setTranslationX)
+ view.animateShow(volumeResources.dialogShowDurationMillis.first())
+ }
+ is VolumeDialogVisibilityModel.Dismissed -> {
+ tracer.traceVisibilityEnd(it)
+ view.animateHide(
+ duration = volumeResources.dialogHideDurationMillis.first(),
+ translationX = calculateTranslationX(view),
+ )
+ dialog.dismiss()
+ }
+ is VolumeDialogVisibilityModel.Invisible -> {
+ // do nothing
+ }
+ }
+ }
+ .launchIn(this)
+ }
+
+ private suspend fun calculateTranslationX(view: View): Float? {
+ return if (view.display.rotation == RotationPolicy.NATURAL_ROTATION) {
+ val dialogGravity = gravityViewModel.dialogGravity.first()
+ val isGravityLeft = (dialogGravity and Gravity.LEFT) == Gravity.LEFT
+ if (isGravityLeft) {
+ -1
+ } else {
+ 1
+ } * view.width / 2.0f
+ } else {
+ null
+ }
+ }
+
+ private suspend fun View.animateShow(duration: Long) {
+ animate()
+ .alpha(1f)
+ .translationX(0f)
+ .setDuration(duration)
+ .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator())
+ .suspendAnimate(jankListenerFactory.show(this, duration))
+ /* TODO(b/369993851)
+ .withEndAction(Runnable {
+ if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
+ if (mRingerIcon != null) {
+ mRingerIcon.postOnAnimationDelayed(
+ getSinglePressFor(mRingerIcon), 1500
+ )
+ }
+ }
+ })
+ */
+ }
+
+ private suspend fun View.animateHide(duration: Long, translationX: Float?) {
+ val animator =
+ animate()
+ .alpha(0f)
+ .setDuration(duration)
+ .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator())
+ /* TODO(b/369993851)
+ .withEndAction(
+ Runnable {
+ mHandler.postDelayed(
+ Runnable {
+ hideRingerDrawer()
+
+ },
+ 50
+ )
+ }
+ )
+ */
+ if (translationX != null) {
+ animator.translationX(translationX)
+ }
+ animator.suspendAnimate(jankListenerFactory.dismiss(this, duration))
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt
new file mode 100644
index 000000000000..9fcd77716fb6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.utils
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.view.View
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import javax.inject.Inject
+
+/** Provides [Animator.AnimatorListener] to measure Volume CUJ Jank */
+@VolumeDialogPluginScope
+class JankListenerFactory
+@Inject
+constructor(private val interactionJankMonitor: InteractionJankMonitor) {
+
+ fun show(view: View, timeout: Long) = getJunkListener(view, "show", timeout)
+
+ fun update(view: View, timeout: Long) = getJunkListener(view, "update", timeout)
+
+ fun dismiss(view: View, timeout: Long) = getJunkListener(view, "dismiss", timeout)
+
+ private fun getJunkListener(
+ view: View,
+ type: String,
+ timeout: Long,
+ ): Animator.AnimatorListener {
+ return object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ interactionJankMonitor.begin(
+ InteractionJankMonitor.Configuration.Builder.withView(
+ Cuj.CUJ_VOLUME_CONTROL,
+ view,
+ )
+ .setTag(type)
+ .setTimeout(timeout)
+ )
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
new file mode 100644
index 000000000000..4eae3b9a8da5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.utils
+
+import android.animation.Animator
+import android.view.ViewPropertyAnimator
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Starts animation and suspends until it's finished. Cancels the animation if the running coroutine
+ * is cancelled.
+ *
+ * Careful! This method overrides [ViewPropertyAnimator.setListener]. Use [animationListener]
+ * instead.
+ */
+suspend fun ViewPropertyAnimator.suspendAnimate(
+ animationListener: Animator.AnimatorListener? = null
+) = suspendCancellableCoroutine { continuation ->
+ start()
+ setListener(
+ object : Animator.AnimatorListener {
+ override fun onAnimationStart(animation: Animator) {
+ animationListener?.onAnimationStart(animation)
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ continuation.resume(Unit)
+ animationListener?.onAnimationEnd(animation)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ animationListener?.onAnimationCancel(animation)
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {
+ animationListener?.onAnimationRepeat(animation)
+ }
+ }
+ )
+ continuation.invokeOnCancellation { this.cancel() }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
index df6523c9d750..112afb1debf5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
@@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
+/** Exposes dialog [GravityInt] for use in the UI layer. */
@VolumeDialogScope
class VolumeDialogGravityViewModel
@Inject
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
index 8aa0d09157ad..f336d469abf6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
@@ -16,15 +16,14 @@
package com.android.systemui.volume.dialog.ui.viewmodel
-import android.app.Dialog
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.plugins.VolumeDialogController
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
-import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
@@ -32,8 +31,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class)
@VolumeDialogPluginScope
@@ -49,6 +47,7 @@ constructor(
override suspend fun onActivated(): Nothing {
coroutineScope {
dialogVisibilityInteractor.dialogVisibility
+ .onEach { controller.notifyVisible(it is VolumeDialogVisibilityModel.Visible) }
.mapLatest { visibilityModel ->
with(visibilityModel) {
if (this is VolumeDialogVisibilityModel.Visible) {
@@ -78,15 +77,8 @@ constructor(
dialogVisibilityInteractor.dismissDialog(Events.DISMISS_REASON_UNKNOWN)
}
}
- launch { dialog.awaitShow() }
+ dialog.show()
Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
}
}
-
-/** Shows [Dialog] until suspend function is cancelled. */
-private suspend fun Dialog.awaitShow() =
- suspendCancellableCoroutine<Unit> {
- show()
- it.invokeOnCancellation { dismiss() }
- }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogResourcesViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogResourcesViewModel.kt
new file mode 100644
index 000000000000..da9be98dd332
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogResourcesViewModel.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.onConfigChanged
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Provides cached resources [Flow]s that update when the configuration changes.
+ *
+ * Consume or use [kotlinx.coroutines.flow.first] to get the value.
+ */
+@VolumeDialogScope
+class VolumeDialogResourcesViewModel
+@Inject
+constructor(
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ @UiBackground private val uiBackgroundContext: CoroutineContext,
+ private val context: Context,
+ private val configurationController: ConfigurationController,
+) {
+
+ val dialogShowDurationMillis: Flow<Long> = configurationResource {
+ getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong()
+ }
+
+ val dialogHideDurationMillis: Flow<Long> = configurationResource {
+ getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong()
+ }
+
+ private fun <T> configurationResource(get: Resources.() -> T): Flow<T> =
+ configurationController.onConfigChanged
+ .map { context.resources.get() }
+ .onStart { emit(context.resources.get()) }
+ .flowOn(uiBackgroundContext)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+ .filterNotNull()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index 30c8c15387eb..84c837c9033d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -17,11 +17,20 @@
package com.android.systemui.volume.dialog.ui.viewmodel
import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
-class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
+/** Provides a state for the Volume Dialog. */
+class VolumeDialogViewModel
+@AssistedInject
+constructor(dialogVisibilityInteractor: VolumeDialogVisibilityInteractor) : ExclusiveActivatable() {
+
+ val dialogVisibilityModel: Flow<VolumeDialogVisibilityModel> =
+ dialogVisibilityInteractor.dialogVisibility
override suspend fun onActivated(): Nothing {
awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt
new file mode 100644
index 000000000000..db35ca7c12ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/utils/VolumeTracer.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.utils
+
+import android.os.Trace
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+
+/** Traces the async sections for the Volume Dialog. */
+interface VolumeTracer {
+
+ fun traceVisibilityStart(model: VolumeDialogVisibilityModel)
+
+ fun traceVisibilityEnd(model: VolumeDialogVisibilityModel)
+}
+
+@VolumeDialogPluginScope
+class VolumeTracerImpl @Inject constructor() : VolumeTracer {
+
+ override fun traceVisibilityStart(model: VolumeDialogVisibilityModel) =
+ with(model) { Trace.beginAsyncSection(methodName, tracingCookie) }
+
+ override fun traceVisibilityEnd(model: VolumeDialogVisibilityModel) =
+ with(model) { Trace.endAsyncSection(methodName, tracingCookie) }
+
+ private val VolumeDialogVisibilityModel.tracingCookie
+ get() = this.hashCode()
+
+ private val VolumeDialogVisibilityModel.methodName
+ get() =
+ when (this) {
+ is VolumeDialogVisibilityModel.Visible -> "VolumeDialog#show"
+ is VolumeDialogVisibilityModel.Dismissed -> "VolumeDialog#dismiss"
+ is VolumeDialogVisibilityModel.Invisible -> error("Invisible is unsupported")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
index f94cbda49e91..609ba0212cef 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
@@ -16,7 +16,10 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+import com.android.settingslib.media.PhoneMediaDevice.inputRoutingEnabledAndIsDesktop
+import android.content.Context
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.volume.domain.interactor.AudioOutputInteractor
import com.android.systemui.volume.domain.interactor.AudioSharingInteractor
import com.android.systemui.volume.domain.model.AudioOutputDevice
@@ -46,6 +49,7 @@ import kotlinx.coroutines.flow.stateIn
class MediaOutputComponentInteractor
@Inject
constructor(
+ @Application private val context: Context,
@VolumePanelScope private val coroutineScope: CoroutineScope,
private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
audioOutputInteractor: AudioOutputInteractor,
@@ -91,7 +95,8 @@ constructor(
MediaOutputComponentModel.Calling(
device = currentAudioDevice,
isInAudioSharing = isInAudioSharing,
- canOpenAudioSwitcher = false,
+ /* allow open switcher when input routing is enabled in desktop */
+ canOpenAudioSwitcher = inputRoutingEnabledAndIsDesktop(context),
)
)
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index ffb1f11c4970..2aa1ac99a400 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -18,6 +18,9 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
import android.content.Context
import android.media.AudioManager
+import android.media.AudioManager.STREAM_ALARM
+import android.media.AudioManager.STREAM_MUSIC
+import android.media.AudioManager.STREAM_NOTIFICATION
import android.util.Log
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
@@ -25,7 +28,11 @@ import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.AudioStreamModel
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.modes.shared.ModesUiIcons
import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
+import com.android.systemui.util.kotlin.combine
import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
import dagger.assisted.Assisted
@@ -51,16 +58,14 @@ constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val context: Context,
private val audioVolumeInteractor: AudioVolumeInteractor,
+ private val zenModeInteractor: ZenModeInteractor,
private val uiEventLogger: UiEventLogger,
private val volumePanelLogger: VolumePanelLogger,
) : SliderViewModel {
private val volumeChanges = MutableStateFlow<Int?>(null)
private val streamsAffectedByRing =
- setOf(
- AudioManager.STREAM_RING,
- AudioManager.STREAM_NOTIFICATION,
- )
+ setOf(AudioManager.STREAM_RING, AudioManager.STREAM_NOTIFICATION)
private val audioStream = audioStreamWrapper.audioStream
private val iconsByStream =
mapOf(
@@ -78,11 +83,6 @@ constructor(
AudioStream(AudioManager.STREAM_NOTIFICATION) to R.string.stream_notification,
AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm,
)
- private val disabledTextByStream =
- mapOf(
- AudioStream(AudioManager.STREAM_NOTIFICATION) to
- R.string.stream_notification_unavailable,
- )
private val uiEventByStream =
mapOf(
AudioStream(AudioManager.STREAM_MUSIC) to
@@ -98,15 +98,48 @@ constructor(
)
override val slider: StateFlow<SliderState> =
- combine(
- audioVolumeInteractor.getAudioStream(audioStream),
- audioVolumeInteractor.canChangeVolume(audioStream),
- audioVolumeInteractor.ringerMode,
- ) { model, isEnabled, ringerMode ->
- volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
- model.toState(isEnabled, ringerMode)
- }
- .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
+ if (ModesUiIcons.isEnabled) {
+ combine(
+ audioVolumeInteractor.getAudioStream(audioStream),
+ audioVolumeInteractor.canChangeVolume(audioStream),
+ audioVolumeInteractor.ringerMode,
+ zenModeInteractor.activeModesBlockingEverything,
+ zenModeInteractor.activeModesBlockingAlarms,
+ zenModeInteractor.activeModesBlockingMedia,
+ ) {
+ model,
+ isEnabled,
+ ringerMode,
+ modesBlockingEverything,
+ modesBlockingAlarms,
+ modesBlockingMedia ->
+ volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
+ model.toState(
+ isEnabled,
+ ringerMode,
+ getStreamDisabledMessage(
+ modesBlockingEverything,
+ modesBlockingAlarms,
+ modesBlockingMedia,
+ ),
+ )
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
+ } else {
+ combine(
+ audioVolumeInteractor.getAudioStream(audioStream),
+ audioVolumeInteractor.canChangeVolume(audioStream),
+ audioVolumeInteractor.ringerMode,
+ ) { model, isEnabled, ringerMode ->
+ volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
+ model.toState(
+ isEnabled,
+ ringerMode,
+ getStreamDisabledMessageWithoutModes(audioStream),
+ )
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
+ }
init {
volumeChanges
@@ -139,6 +172,7 @@ constructor(
private fun AudioStreamModel.toState(
isEnabled: Boolean,
ringerMode: RingerMode,
+ disabledMessage: String?,
): State {
val label =
labelsByStream[audioStream]?.let(context::getString)
@@ -148,13 +182,7 @@ constructor(
valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
icon = getIcon(ringerMode),
label = label,
- disabledMessage =
- context.getString(
- disabledTextByStream.getOrDefault(
- audioStream,
- R.string.stream_alarm_unavailable,
- )
- ),
+ disabledMessage = disabledMessage,
isEnabled = isEnabled,
a11yStep = volumeRange.step,
a11yClickDescription =
@@ -191,6 +219,43 @@ constructor(
)
}
+ private fun getStreamDisabledMessage(
+ blockingEverything: ActiveZenModes,
+ blockingAlarms: ActiveZenModes,
+ blockingMedia: ActiveZenModes,
+ ): String {
+ // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
+ // In fact, VOICE_CALL should not be affected by interruption filtering at all.
+ return if (audioStream.value == STREAM_NOTIFICATION) {
+ context.getString(R.string.stream_notification_unavailable)
+ } else {
+ val blockingModeName =
+ when {
+ blockingEverything.mainMode != null -> blockingEverything.mainMode.name
+ audioStream.value == STREAM_ALARM -> blockingAlarms.mainMode?.name
+ audioStream.value == STREAM_MUSIC -> blockingMedia.mainMode?.name
+ else -> null
+ }
+
+ if (blockingModeName != null) {
+ context.getString(R.string.stream_unavailable_by_modes, blockingModeName)
+ } else {
+ // Should not actually be visible, but as a catch-all.
+ context.getString(R.string.stream_unavailable_by_unknown)
+ }
+ }
+ }
+
+ private fun getStreamDisabledMessageWithoutModes(audioStream: AudioStream): String {
+ // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
+ // In fact, VOICE_CALL should not be affected by interruption filtering at all.
+ return if (audioStream.value == STREAM_NOTIFICATION) {
+ context.getString(R.string.stream_notification_unavailable)
+ } else {
+ context.getString(R.string.stream_alarm_unavailable)
+ }
+ }
+
private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon {
val iconRes =
if (isAffectedByMute && isMuted) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 5e37d4cd1faf..51a7b5f6f979 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -338,6 +338,25 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase {
assertThat(mController.mFloatingMenu).isInstanceOf(MenuViewLayerController.class);
}
+ @Test
+ public void onUserInitializationComplete_destroysOldWidget() {
+ enableAccessibilityFloatingMenuConfig();
+ mController = setUpController();
+
+ captureKeyguardUpdateMonitorCallback();
+ mKeyguardCallback.onUserUnlocked();
+ mKeyguardCallback.onKeyguardVisibilityChanged(false);
+
+ IAccessibilityFloatingMenu floatingMenu = mController.mFloatingMenu;
+
+ mController.mUserInitializationCompleteCallback
+ .onUserInitializationComplete(mContext.getUserId());
+ mTestableLooper.processAllMessages();
+
+ assertThat(mController.mFloatingMenu).isNotNull();
+ assertThat(mController.mFloatingMenu).isNotSameInstanceAs(floatingMenu);
+ }
+
private AccessibilityFloatingMenuController setUpController() {
final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
final ViewCaptureAwareWindowManager viewCaptureAwareWindowManager =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index 6c42662f2cdd..762cfa03c310 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -52,14 +52,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"
private val emulationSpec =
- DeviceEmulationSpec(
- DisplaySpec(
- "phone",
- width = 320,
- height = 690,
- densityDpi = 160,
- )
- )
+ DeviceEmulationSpec(DisplaySpec("phone", width = 320, height = 690, densityDpi = 160))
}
private val kosmos = Kosmos()
@@ -68,7 +61,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
TransitionAnimator(
kosmos.fakeExecutor,
ActivityTransitionAnimator.TIMINGS,
- ActivityTransitionAnimator.INTERPOLATORS
+ ActivityTransitionAnimator.INTERPOLATORS,
)
@get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
@@ -131,16 +124,17 @@ class TransitionAnimatorTest : SysuiTestCase() {
waitForIdleSync()
val controller = TestController(transitionContainer, isLaunching)
- val animator =
- transitionAnimator.createAnimator(
+ val animation =
+ transitionAnimator.createAnimation(
controller,
+ controller.createAnimatorState(),
createEndState(transitionContainer),
backgroundLayer,
- fadeWindowBackgroundLayer
- )
+ fadeWindowBackgroundLayer,
+ ) as TransitionAnimator.InterpolatedAnimation
return AnimatorSet().apply {
- duration = animator.duration
- play(animator)
+ duration = animation.animator.duration
+ play(animation.animator)
}
}
@@ -153,13 +147,13 @@ class TransitionAnimatorTest : SysuiTestCase() {
right = containerLocation[0] + emulationSpec.display.width,
bottom = containerLocation[1] + emulationSpec.display.height,
topCornerRadius = 0f,
- bottomCornerRadius = 0f
+ bottomCornerRadius = 0f,
)
}
private fun recordMotion(
backgroundLayer: GradientDrawable,
- animator: AnimatorSet
+ animator: AnimatorSet,
): RecordedMotion {
return motionRule.record(
animator,
@@ -167,7 +161,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
feature(DrawableFeatureCaptures.bounds, "bounds")
feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
feature(DrawableFeatureCaptures.alpha, "alpha")
- }
+ },
)
}
}
@@ -178,7 +172,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
*/
private class TestController(
override var transitionContainer: ViewGroup,
- override val isLaunching: Boolean
+ override val isLaunching: Boolean,
) : TransitionAnimator.Controller {
override fun createAnimatorState(): TransitionAnimator.State {
val containerLocation = IntArray(2)
@@ -189,7 +183,7 @@ private class TestController(
right = containerLocation[0] + 200,
bottom = containerLocation[1] + 400,
topCornerRadius = 10f,
- bottomCornerRadius = 20f
+ bottomCornerRadius = 20f,
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
index 476d6e373df3..7c0892891707 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java
@@ -103,11 +103,10 @@ public class AppOpsControllerTest extends SysuiTestCase {
@Mock()
private BroadcastDispatcher mDispatcher;
@Mock(stubOnly = true)
- private AudioManager.AudioRecordingCallback mRecordingCallback;
- @Mock(stubOnly = true)
private AudioRecordingConfiguration mPausedMockRecording;
private AppOpsControllerImpl mController;
+ private AudioManager.AudioRecordingCallback mRecordingCallback;
private TestableLooper mTestableLooper;
private final FakeExecutor mBgExecutor = new FakeExecutor(new FakeSystemClock());
@@ -975,6 +974,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
}
private void verifyUnPausedSentActive(int micOpCode) {
+ // Setup stubs the initial active recording list with a single silenced client
mController.addCallback(new int[]{micOpCode}, mCallback);
mBgExecutor.runAllReady();
mTestableLooper.processAllMessages();
@@ -982,11 +982,20 @@ public class AppOpsControllerTest extends SysuiTestCase {
TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
- mRecordingCallback.onRecordingConfigChanged(Collections.emptyList());
+
+ // Update with multiple recording configs, of which one is unsilenced
+ var mockARCUnsilenced = mock(AudioRecordingConfiguration.class);
+ when(mockARCUnsilenced.getClientUid()).thenReturn(TEST_UID);
+ when(mockARCUnsilenced.isClientSilenced()).thenReturn(false);
+
+ mRecordingCallback.onRecordingConfigChanged(List.of(
+ mockARCUnsilenced, mPausedMockRecording));
mTestableLooper.processAllMessages();
verify(mCallback).onActiveStateChanged(micOpCode, TEST_UID, TEST_PACKAGE_NAME, true);
+ // For consistency since this runs in a loop
+ mController.removeCallback(new int[]{micOpCode}, mCallback);
}
private void verifyAudioPausedSentInactive(int micOpCode) {
@@ -997,11 +1006,16 @@ public class AppOpsControllerTest extends SysuiTestCase {
TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
- AudioRecordingConfiguration mockARC = mock(AudioRecordingConfiguration.class);
- when(mockARC.getClientUid()).thenReturn(TEST_UID_OTHER);
- when(mockARC.isClientSilenced()).thenReturn(true);
+ // Multiple recording configs, which are all silenced
+ AudioRecordingConfiguration mockARCOne = mock(AudioRecordingConfiguration.class);
+ when(mockARCOne.getClientUid()).thenReturn(TEST_UID_OTHER);
+ when(mockARCOne.isClientSilenced()).thenReturn(true);
+
+ AudioRecordingConfiguration mockARCTwo = mock(AudioRecordingConfiguration.class);
+ when(mockARCTwo.getClientUid()).thenReturn(TEST_UID_OTHER);
+ when(mockARCTwo.isClientSilenced()).thenReturn(true);
- mRecordingCallback.onRecordingConfigChanged(List.of(mockARC));
+ mRecordingCallback.onRecordingConfigChanged(List.of(mockARCOne, mockARCTwo));
mTestableLooper.processAllMessages();
InOrder inOrder = inOrder(mCallback);
@@ -1009,6 +1023,8 @@ public class AppOpsControllerTest extends SysuiTestCase {
micOpCode, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
inOrder.verify(mCallback).onActiveStateChanged(
micOpCode, TEST_UID_OTHER, TEST_PACKAGE_NAME, false);
+ // For consistency since this runs in a loop
+ mController.removeCallback(new int[]{micOpCode}, mCallback);
}
private void verifySingleActiveOps(int op) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 7889b3cd6cc3..7889b3cd6cc3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index 9fbe09619ff1..9fbe09619ff1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
new file mode 100644
index 000000000000..655b2cc2dece
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AudioSharingButtonViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val bluetoothState = MutableStateFlow(false)
+ private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
+ @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+ @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+ @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
+ @Mock private lateinit var deviceItem: DeviceItem
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var audioSharingButtonViewModel: AudioSharingButtonViewModel
+
+ @Before
+ fun setUp() {
+ mockitoSession =
+ mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
+ whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState)
+ whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
+ audioSharingButtonViewModel =
+ AudioSharingButtonViewModel(
+ localBluetoothManager,
+ kosmos.audioSharingInteractor,
+ bluetoothStateInteractor,
+ deviceItemInteractor,
+ )
+ audioSharingButtonViewModel.activateIn(testScope)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun testButtonStateUpdate_bluetoothOff_returnGone() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+
+ runCurrent()
+
+ assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_noDevice_returnGone() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothState.value = true
+ runCurrent()
+
+ assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothState.value = true
+ runCurrent()
+ deviceItemUpdate.emit(listOf())
+ runCurrent()
+ kosmos.bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ runCurrent()
+
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
+ isActive = true,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_hasSource_returnGone() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ )
+ .thenReturn(true)
+ bluetoothState.value = true
+ runCurrent()
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ )
+ .thenReturn(false)
+ whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true)
+ bluetoothState.value = true
+ runCurrent()
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button,
+ isActive = false,
+ )
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
new file mode 100644
index 000000000000..ce37eee24e2a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.flags.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ private lateinit var actionInteractorImpl: DeviceItemActionInteractor
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem
+ private lateinit var connectedMediaDeviceItem: DeviceItem
+ @Mock private lateinit var dialog: SystemUIDialog
+ @Mock private lateinit var leAudioProfile: LeAudioProfile
+ @Mock private lateinit var bluetoothDevice: BluetoothDevice
+
+ @Before
+ fun setUp() {
+ mockitoSession =
+ mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
+ connectedMediaDeviceItem =
+ DeviceItem(
+ type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = null,
+ background = null,
+ )
+ connectedAudioSharingMediaDeviceItem =
+ DeviceItem(
+ type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = null,
+ background = null,
+ )
+ actionInteractorImpl = kosmos.audioSharingDeviceItemActionInteractorImpl
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT)
+ fun testOnClick_connectedAudioSharingMediaDevice_flagOn_createDialog() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
+ verify(dialogTransitionAnimator)
+ .showFromDialog(any(), any(), eq(null), anyBoolean())
+ }
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT)
+ fun testOnClick_connectedAudioSharingMediaDevice_flagOff_shouldLaunchSettings() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
+ actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
+ verify(activityStarter)
+ .postStartActivityDismissingKeyguard(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ )
+ verify(dialogTransitionAnimator, never())
+ .showFromDialog(any(), any(), eq(null), anyBoolean())
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(cachedBluetoothDevice.uiAccessibleProfiles)
+ .thenReturn(listOf(leAudioProfile))
+ whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ )
+ )
+ .thenReturn(true)
+
+ actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
+ verify(activityStarter, Mockito.never())
+ .postStartActivityDismissingKeyguard(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
+ whenever(cachedBluetoothDevice.uiAccessibleProfiles)
+ .thenReturn(listOf(leAudioProfile))
+
+ whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ )
+ )
+ .thenReturn(false)
+
+ actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
+ verify(activityStarter)
+ .postStartActivityDismissingKeyguard(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ )
+ }
+ }
+ }
+
+ private companion object {
+ const val DEVICE_NAME = "device"
+ const val DEVICE_CONNECTION_SUMMARY = "active"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
new file mode 100644
index 000000000000..25b85b514435
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.widget.Button
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDialogDelegateTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
+ private val updateFlow = MutableSharedFlow<Unit>()
+ private lateinit var underTest: AudioSharingDialogDelegate
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ // TODO(b/364515243): use real object instead of mock
+ whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow)
+ whenever(deviceItemInteractor.deviceItemUpdate)
+ .thenReturn(MutableStateFlow(emptyList()))
+ underTest = audioSharingDialogDelegate
+ }
+ }
+
+ @Test
+ fun testCreateDialog() =
+ kosmos.testScope.runTest {
+ val dialog = underTest.createDialog()
+ assertThat(dialog).isInstanceOf(SystemUIDialog::class.java)
+ }
+
+ @Test
+ fun testCreateDialog_showState() =
+ with(kosmos) {
+ testScope.runTest {
+ val availableDeviceName = "name"
+ whenever(cachedBluetoothDevice.name).thenReturn(availableDeviceName)
+ val dialog = spy(underTest.createDialog())
+ dialog.show()
+ runCurrent()
+ val subtitleTextView = dialog.findViewById<TextView>(R.id.subtitle)
+ val switchActiveButton = dialog.findViewById<Button>(R.id.switch_active_button)
+ val shareAudioButton = dialog.findViewById<Button>(R.id.share_audio_button)
+ val subtitle =
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ availableDeviceName,
+ ""
+ )
+ val switchButtonText =
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ availableDeviceName
+ )
+ assertThat(subtitleTextView.text).isEqualTo(subtitle)
+ assertThat(switchActiveButton.text).isEqualTo(switchButtonText)
+ assertThat(switchActiveButton.hasOnClickListeners()).isTrue()
+ assertThat(shareAudioButton.hasOnClickListeners()).isTrue()
+
+ switchActiveButton.performClick()
+ verify(dialog).dismiss()
+ }
+ }
+
+ @Test
+ fun testCreateDialog_hideState() =
+ with(kosmos) {
+ testScope.runTest {
+ val dialog = spy(underTest.createDialog())
+ dialog.show()
+ runCurrent()
+ updateFlow.emit(Unit)
+ runCurrent()
+ verify(dialog).dismiss()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt
new file mode 100644
index 000000000000..beb816cae095
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bluetooth.cachedBluetoothDeviceManager
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDialogViewModelTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+ @Mock private lateinit var leAudioProfile: LeAudioProfile
+ private val updateFlow = MutableSharedFlow<Unit>()
+ private lateinit var underTest: AudioSharingDialogViewModel
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ // TODO(b/364515243): use real object instead of mock
+ whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow)
+ whenever(deviceItemInteractor.deviceItemUpdate)
+ .thenReturn(MutableStateFlow(emptyList()))
+ underTest = audioSharingDialogViewModel
+ }
+ }
+
+ @Test
+ fun testDialogState_show() =
+ with(kosmos) {
+ testScope.runTest {
+ val deviceName = "name"
+ whenever(cachedBluetoothDevice.name).thenReturn(deviceName)
+ val actual by collectLastValue(underTest.dialogState)
+ runCurrent()
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingDialogState.Show(
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ deviceName,
+ ""
+ ),
+ context.getString(
+ R.string
+ .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ deviceName
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDialogState_showWithActiveDeviceName() =
+ with(kosmos) {
+ testScope.runTest {
+ val deviceName = "name"
+ whenever(cachedBluetoothDevice.name).thenReturn(deviceName)
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(localBluetoothManager.cachedDeviceManager)
+ .thenReturn(cachedBluetoothDeviceManager)
+ whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
+ whenever(leAudioProfile.activeDevices).thenReturn(listOf(mock<BluetoothDevice>()))
+ whenever(cachedBluetoothDeviceManager.findDevice(any()))
+ .thenReturn(cachedBluetoothDevice)
+ val actual by collectLastValue(underTest.dialogState)
+ runCurrent()
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingDialogState.Show(
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ deviceName,
+ deviceName
+ ),
+ context.getString(
+ R.string
+ .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ deviceName
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDialogState_hide() =
+ with(kosmos) {
+ testScope.runTest {
+ val actual by collectLastValue(underTest.dialogState)
+ runCurrent()
+ updateFlow.emit(Unit)
+ assertThat(actual).isEqualTo(AudioSharingDialogState.Hide)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
index 2c53fd67c89d..25f956574274 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
@@ -16,158 +16,197 @@
package com.android.systemui.bluetooth.qsdialog
+import android.bluetooth.BluetoothLeBroadcast
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.res.R
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
-@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
class AudioSharingInteractorTest : SysuiTestCase() {
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
- private val bluetoothState = MutableStateFlow(false)
- private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
- @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
- @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
- @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
- @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
- @Mock private lateinit var deviceItem: DeviceItem
- private lateinit var mockitoSession: StaticMockitoSession
- private lateinit var audioSharingInteractor: AudioSharingInteractor
+ @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
+ @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
+ private lateinit var underTest: AudioSharingInteractor
@Before
fun setUp() {
- mockitoSession =
- mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
- whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState)
- whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
- audioSharingInteractor =
- AudioSharingInteractor(
- localBluetoothManager,
- bluetoothStateInteractor,
- deviceItemInteractor,
- testScope.backgroundScope,
- testDispatcher,
- )
+ with(kosmos) { underTest = audioSharingInteractor }
}
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
- }
+ @Test
+ fun testIsAudioSharingOn_flagOff_false() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
+ val value by collectLastValue(underTest.isAudioSharingOn)
+ runCurrent()
+
+ assertThat(value).isFalse()
+ }
+ }
@Test
- fun testButtonStateUpdate_bluetoothOff_returnGone() {
- testScope.runTest {
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
+ fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ val value by collectLastValue(underTest.isAudioSharingOn)
+ runCurrent()
+
+ assertThat(value).isFalse()
+ }
+ }
- assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ @Test
+ fun testIsAudioSharingOn_flagOn_inAudioSharing_true() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ val value by collectLastValue(underTest.isAudioSharingOn)
+ runCurrent()
+
+ assertThat(value).isTrue()
+ }
}
- }
@Test
- fun testButtonStateUpdate_noDevice_returnGone() {
- testScope.runTest {
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- runCurrent()
+ fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ val value by collectLastValue(underTest.audioSourceStateUpdate)
+ runCurrent()
+
+ assertThat(value).isNull()
+ }
+ }
- assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ @Test
+ fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ val value by collectLastValue(underTest.audioSourceStateUpdate)
+ runCurrent()
+ bluetoothTileDialogAudioSharingRepository.emitAudioSourceStateUpdate()
+ runCurrent()
+
+ assertThat(value).isNull()
+ }
}
- }
@Test
- fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() {
- testScope.runTest {
- whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(true)
-
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- deviceItemUpdate.emit(listOf())
- runCurrent()
-
- assertThat(actual)
- .isEqualTo(
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
- isActive = true
- )
- )
+ fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
}
- }
@Test
- fun testButtonStateUpdate_hasSource_returnGone() {
- testScope.runTest {
- whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false)
- whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- cachedBluetoothDevice,
- localBluetoothManager
- )
- )
- .thenReturn(true)
+ fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(null)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
+ }
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- deviceItemUpdate.emit(listOf(deviceItem))
- runCurrent()
+ @Test
+ fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+ localBluetoothLeBroadcast
+ )
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
- assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
}
- }
@Test
- fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() {
- testScope.runTest {
- whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false)
- whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- cachedBluetoothDevice,
- localBluetoothManager
- )
+ fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+ localBluetoothLeBroadcast
)
- .thenReturn(false)
- whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true)
-
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- deviceItemUpdate.emit(listOf(deviceItem))
- runCurrent()
-
- assertThat(actual)
- .isEqualTo(
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button,
- isActive = false
- )
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+ verify(localBluetoothLeBroadcast)
+ .registerServiceCallBack(any(), callbackCaptor.capture())
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
+ }
+
+ @Test
+ fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+ localBluetoothLeBroadcast
)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+ verify(localBluetoothLeBroadcast)
+ .registerServiceCallBack(any(), callbackCaptor.capture())
+ runCurrent()
+ callbackCaptor.value.onPlaybackStarted(0, 0)
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue()
+ job.cancel()
+ }
}
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
new file mode 100644
index 000000000000..c9e88136e4a0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioSharingRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AudioSharingRepositoryTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+ @Mock private lateinit var leAudioBroadcastProfile: LocalBluetoothLeBroadcast
+ @Mock private lateinit var leAudioBroadcastAssistant: LocalBluetoothLeBroadcastAssistant
+ @Mock private lateinit var metadata: BluetoothLeBroadcastMetadata
+ @Mock private lateinit var bluetoothDevice: BluetoothDevice
+ private val kosmos = testKosmos()
+ private lateinit var underTest: AudioSharingRepository
+
+ @Before
+ fun setUp() {
+ underTest =
+ AudioSharingRepositoryImpl(
+ kosmos.localBluetoothManager,
+ kosmos.audioSharingRepository,
+ kosmos.testDispatcher,
+ )
+ }
+
+ @Test
+ fun testSwitchActive() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(true)
+ underTest.setActive(cachedBluetoothDevice)
+ verify(cachedBluetoothDevice).setActive()
+ }
+ }
+
+ @Test
+ fun testSwitchActive_flagOff_doNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+ underTest.setActive(cachedBluetoothDevice)
+ verify(cachedBluetoothDevice, never()).setActive()
+ }
+ }
+
+ @Test
+ fun testStartAudioSharing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ underTest.startAudioSharing()
+ verify(leAudioBroadcastProfile).startPrivateBroadcast()
+ }
+ }
+
+ @Test
+ fun testStartAudioSharing_flagOff_doNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+ underTest.startAudioSharing()
+ verify(leAudioBroadcastProfile, never()).startPrivateBroadcast()
+ }
+ }
+
+ @Test
+ fun testAddSource_flagOff_doesNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant, never()).allConnectedDevices
+ }
+ }
+
+ @Test
+ fun testAddSource_noMetadata_doesNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+ .thenReturn(null)
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant, never()).allConnectedDevices
+ }
+ }
+
+ @Test
+ fun testAddSource_noConnectedDevice_doesNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ whenever(profileManager.leAudioBroadcastAssistantProfile)
+ .thenReturn(leAudioBroadcastAssistant)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+ .thenReturn(metadata)
+ whenever(leAudioBroadcastAssistant.allConnectedDevices).thenReturn(emptyList())
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant, never()).addSource(any(), any(), anyBoolean())
+ }
+ }
+
+ @Test
+ fun testAddSource_hasConnectedDeviceAndMetadata_addSource() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ whenever(profileManager.leAudioBroadcastAssistantProfile)
+ .thenReturn(leAudioBroadcastAssistant)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+ .thenReturn(metadata)
+ whenever(leAudioBroadcastAssistant.allConnectedDevices)
+ .thenReturn(listOf(bluetoothDevice))
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant).addSource(bluetoothDevice, metadata, false)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index d7bea6680c2d..a56c2cb25542 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -31,8 +31,11 @@ import com.android.settingslib.flags.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.getMutableStateFlow
@@ -42,12 +45,12 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -64,10 +67,12 @@ import org.mockito.junit.MockitoRule
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
@EnableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
private val fakeSystemClock = FakeSystemClock()
private val backgroundExecutor = FakeExecutor(fakeSystemClock)
@@ -75,8 +80,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
- @Mock private lateinit var audioSharingInteractor: AudioSharingInteractor
-
@Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor
@Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@@ -111,15 +114,15 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
private val sharedPreferences = FakeSharedPreferences()
- private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var testScope: TestScope
@Before
fun setUp() {
- scheduler = TestCoroutineScheduler()
- dispatcher = UnconfinedTestDispatcher(scheduler)
- testScope = TestScope(dispatcher)
+ dispatcher = kosmos.testDispatcher
+ testScope = kosmos.testScope
+ // TODO(b/364515243): use real object instead of mock
+ whenever(kosmos.deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
bluetoothTileDialogViewModel =
BluetoothTileDialogViewModel(
deviceItemInteractor,
@@ -139,11 +142,13 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
dispatcher
)
),
- audioSharingInteractor,
+ kosmos.audioSharingInteractor,
+ kosmos.audioSharingButtonViewModelFactory,
bluetoothDeviceMetadataInteractor,
mDialogTransitionAnimator,
activityStarter,
uiEventLogger,
+ bluetoothTileDialogLogger,
testScope.backgroundScope,
dispatcher,
dispatcher,
@@ -161,13 +166,10 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
whenever(sysuiDialog.context).thenReturn(mContext)
whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
.thenReturn(getMutableStateFlow(false))
- whenever(bluetoothTileDialogDelegate.deviceItemClick)
- .thenReturn(getMutableStateFlow(deviceItem))
+ whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow())
whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0))
whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle)
.thenReturn(getMutableStateFlow(false))
- whenever(audioSharingInteractor.audioSharingButtonStateUpdate)
- .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone))
whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
}
@@ -175,6 +177,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
fun testShowDialog_noAnimation() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(null)
+ runCurrent()
verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
}
@@ -184,6 +187,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
fun testShowDialog_animated() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(expandable)
+ runCurrent()
verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@@ -194,6 +198,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
testScope.runTest {
backgroundExecutor.execute {
bluetoothTileDialogViewModel.showDialog(expandable)
+ runCurrent()
verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@@ -204,6 +209,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
fun testShowDialog_fetchDeviceItem() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(null)
+ runCurrent()
verify(deviceItemInteractor).deviceItemUpdate
}
@@ -214,6 +220,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
testScope.runTest {
whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
bluetoothTileDialogViewModel.showDialog(null)
+ runCurrent()
val clickedView = View(context)
bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
index 681ea754e630..9c427c6b085e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
@@ -15,34 +15,22 @@
*/
package com.android.systemui.bluetooth.qsdialog
-import android.bluetooth.BluetoothDevice
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.LeAudioProfile
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
-import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
import org.mockito.Mock
-import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -56,28 +44,18 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
private lateinit var actionInteractorImpl: DeviceItemActionInteractor
- private lateinit var mockitoSession: StaticMockitoSession
private lateinit var activeMediaDeviceItem: DeviceItem
private lateinit var notConnectedDeviceItem: DeviceItem
- private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem
private lateinit var connectedMediaDeviceItem: DeviceItem
private lateinit var connectedOtherDeviceItem: DeviceItem
@Mock private lateinit var dialog: SystemUIDialog
- @Mock private lateinit var profileManager: LocalBluetoothProfileManager
- @Mock private lateinit var leAudioProfile: LeAudioProfile
- @Mock private lateinit var assistantProfile: LocalBluetoothLeBroadcastAssistant
- @Mock private lateinit var bluetoothDevice: BluetoothDevice
- @Mock private lateinit var bluetoothDeviceGroupId2: BluetoothDevice
- @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
@Before
fun setUp() {
- mockitoSession =
- mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
activeMediaDeviceItem =
DeviceItem(
type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
@@ -86,7 +64,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
notConnectedDeviceItem =
DeviceItem(
type = DeviceItemType.SAVED_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
@@ -95,16 +73,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
connectedMediaDeviceItem =
DeviceItem(
type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
- deviceName = DEVICE_NAME,
- connectionSummary = DEVICE_CONNECTION_SUMMARY,
- iconWithDescription = null,
- background = null
- )
- connectedAudioSharingMediaDeviceItem =
- DeviceItem(
- type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
@@ -113,18 +82,13 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
connectedOtherDeviceItem =
DeviceItem(
type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
background = null
)
- actionInteractorImpl = kosmos.deviceItemActionInteractor
- }
-
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
+ actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
}
@Test
@@ -132,14 +96,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
verify(cachedBluetoothDevice).setActive()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
- )
}
}
}
@@ -149,14 +107,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(activeMediaDeviceItem, dialog)
verify(cachedBluetoothDevice).disconnect()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE
- )
}
}
}
@@ -166,14 +118,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(connectedOtherDeviceItem, dialog)
verify(cachedBluetoothDevice).disconnect()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
- )
}
}
}
@@ -183,293 +129,8 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
verify(cachedBluetoothDevice).connect()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.SAVED_BLUETOOTH_DEVICE
- )
- }
- }
- }
-
- @Test
- fun testOnClick_connectedAudioSharingMediaDevice_logClick() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE
- )
- }
- }
- }
-
- @Test
- fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.uiAccessibleProfiles)
- .thenReturn(listOf(leAudioProfile))
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(true)
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
- whenever(cachedBluetoothDevice.uiAccessibleProfiles)
- .thenReturn(listOf(leAudioProfile))
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(false)
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter)
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_noConnectedLeDevice_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice))
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile))
- whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice))
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter)
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice))
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2))
- whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer {
- val device = it.arguments.first() as BluetoothDevice
- if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2
- }
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasTwoConnectedLeDevice_clickedActiveLe_shouldLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile))
- whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2))
- whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer {
- val device = it.arguments.first() as BluetoothDevice
- if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2
- }
-
- actionInteractorImpl.onClick(activeMediaDeviceItem, dialog)
- verify(activityStarter)
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
}
}
}
@@ -478,7 +139,5 @@ class DeviceItemActionInteractorTest : SysuiTestCase() {
const val DEVICE_NAME = "device"
const val DEVICE_CONNECTION_SUMMARY = "active"
const val DEVICE_ADDRESS = "address"
- const val GROUP_ID_1 = 1
- const val GROUP_ID_2 = 2
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index ef441c1dc12c..10c3457066cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -133,8 +133,8 @@ class DeviceItemFactoryTest : SysuiTestCase() {
@Test
fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast
- // source or assistant.
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast
+ // source or assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
assertThat(
@@ -145,9 +145,9 @@ class DeviceItemFactoryTest : SysuiTestCase() {
}
@Test
- fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_returnsFalse() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
- // assistant.
+ fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_false() {
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+ // assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
`when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(true)
@@ -159,9 +159,9 @@ class DeviceItemFactoryTest : SysuiTestCase() {
}
@Test
- fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_returnsFalse() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
- // assistant.
+ fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_false() {
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+ // assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
`when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false)
`when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true)
@@ -177,8 +177,8 @@ class DeviceItemFactoryTest : SysuiTestCase() {
@Test
fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_returnsTrue() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
- // assistant.
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+ // assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
`when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false)
`when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index 194590c1f626..c39b9a606cfe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -83,18 +83,6 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun setUp() {
dispatcher = UnconfinedTestDispatcher()
testScope = TestScope(dispatcher)
- interactor =
- DeviceItemInteractor(
- bluetoothTileDialogRepository,
- audioManager,
- adapter,
- localBluetoothManager,
- fakeSystemClock,
- logger,
- testScope.backgroundScope,
- dispatcher
- )
-
`when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
`when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
`when`(cachedDevice1.address).thenReturn("ADDRESS")
@@ -108,9 +96,19 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun testUpdateDeviceItems_noCachedDevice_returnEmpty() {
testScope.runTest {
`when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ true }, deviceItem1))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ true }, deviceItem1)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -125,9 +123,19 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
testScope.runTest {
`when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ false }, deviceItem1))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ false }, deviceItem1)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -142,9 +150,19 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
testScope.runTest {
`when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ true }, deviceItem1))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ true }, deviceItem1)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -159,9 +177,22 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
testScope.runTest {
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(
+ createFactory({ false }, deviceItem1),
+ createFactory({ true }, deviceItem2)
+ ),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -176,18 +207,31 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun testUpdateDeviceItems_sortByDisplayPriority() {
testScope.runTest {
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
- interactor.setDeviceItemFactoryListForTesting(
- listOf(
- createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
- createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
- )
- )
- interactor.setDisplayPriorityForTesting(
- listOf(
- DeviceItemType.SAVED_BLUETOOTH_DEVICE,
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device1 },
+ deviceItem1
+ ),
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device2 },
+ deviceItem2
+ )
+ ),
+ listOf(
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+ ),
+ testScope.backgroundScope,
+ dispatcher
)
- )
`when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
`when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
@@ -204,15 +248,28 @@ class DeviceItemInteractorTest : SysuiTestCase() {
fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() {
testScope.runTest {
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
- interactor.setDeviceItemFactoryListForTesting(
- listOf(
- createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
- createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device1 },
+ deviceItem1
+ ),
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device2 },
+ deviceItem2
+ )
+ ),
+ listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE),
+ testScope.backgroundScope,
+ dispatcher
)
- )
- interactor.setDisplayPriorityForTesting(
- listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
- )
`when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
`when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
@@ -231,10 +288,19 @@ class DeviceItemInteractorTest : SysuiTestCase() {
`when`(bluetoothTileDialogRepository.cachedDevices)
.thenReturn(listOf(cachedDevice2, cachedDevice2, cachedDevice2, cachedDevice2))
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ true }, deviceItem2))
- )
-
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ true }, deviceItem2)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
index 85e8ab43b2ee..5741d64c4d32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
@@ -122,6 +122,7 @@ class ClipboardModelTest : SysuiTestCase() {
@Test
@Throws(IOException::class)
+ @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE)
fun test_imageClipData_loadFailure() {
whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
whenever(mMockContext.resources).thenReturn(mContext.resources)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
index edc8c837bf78..edc8c837bf78 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index 2312bbd2d7f8..2312bbd2d7f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
new file mode 100644
index 000000000000..ff3186abecdc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import android.content.testableContext
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import android.view.mockWindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val fakeDisplayRepository = kosmos.displayRepository
+ private val testScope = kosmos.testScope
+
+ private val applicationContext = kosmos.testableContext
+ private val applicationWindowManager = kosmos.mockWindowManager
+
+ private val repo =
+ DisplayWindowPropertiesRepositoryImpl(
+ kosmos.applicationCoroutineScope,
+ applicationContext,
+ applicationWindowManager,
+ fakeDisplayRepository,
+ )
+
+ @Before
+ fun start() {
+ repo.start()
+ }
+
+ @Before
+ fun addDisplays() = runBlocking {
+ fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+ }
+
+ @Test
+ fun get_defaultDisplayId_returnsDefaultProperties() =
+ testScope.runTest {
+ val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(displayContext)
+ .isEqualTo(
+ DisplayWindowProperties(
+ displayId = DEFAULT_DISPLAY_ID,
+ windowType = WINDOW_TYPE_FOO,
+ context = applicationContext,
+ windowManager = applicationWindowManager,
+ )
+ )
+ }
+
+ @Test
+ fun get_nonDefaultDisplayId_returnsNewStatusBarContext() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(displayContext.context).isNotSameInstanceAs(applicationContext)
+ }
+
+ @Test
+ fun get_nonDefaultDisplayId_returnsNewWindowManager() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(displayContext.windowManager).isNotSameInstanceAs(applicationWindowManager)
+ }
+
+ @Test
+ fun get_multipleCallsForDefaultDisplay_returnsSameInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+ .isSameInstanceAs(displayContext)
+ }
+
+ @Test
+ fun get_multipleCallsForNonDefaultDisplay_returnsSameInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+ .isSameInstanceAs(displayContext)
+ }
+
+ @Test
+ fun get_multipleCalls_differentType_returnsNewInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_BAR))
+ .isNotSameInstanceAs(displayContext)
+ }
+
+ @Test
+ fun get_afterDisplayRemoved_returnsNewInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+
+ assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+ .isNotSameInstanceAs(displayContext)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun get_nonExistingDisplayId_throws() =
+ testScope.runTest { repo.get(NON_EXISTING_DISPLAY_ID, WINDOW_TYPE_FOO) }
+
+ private fun createDisplay(displayId: Int) =
+ mock<Display> { on { getDisplayId() } doReturn displayId }
+
+ companion object {
+ private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
+ private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
+ private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
+ private const val WINDOW_TYPE_FOO = 123
+ private const val WINDOW_TYPE_BAR = 321
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 8b1341114c68..8b1341114c68 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 6608542980b0..b3cccea97e08 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -26,6 +26,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
import static com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR;
+import static com.android.systemui.Flags.FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY;
import static com.android.systemui.Flags.FLAG_SIM_PIN_BOUNCER_RESET;
import static com.android.systemui.keyguard.KeyguardViewMediator.DELAYED_KEYGUARD_ACTION;
import static com.android.systemui.keyguard.KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT;
@@ -63,6 +64,7 @@ import android.content.Context;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.telephony.TelephonyManager;
import android.testing.AndroidTestingRunner;
@@ -841,6 +843,32 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
@Test
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+ @EnableFlags(FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY)
+ public void testCancelKeyguardExitAnimationDueToSleep_withPendingLockAndRelockFlag_keyguardWillBeShowing() {
+ startMockKeyguardExitAnimation();
+
+ mViewMediator.onStartedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
+ mViewMediator.onFinishedGoingToSleep(PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, false);
+
+ cancelMockKeyguardExitAnimation();
+
+ mViewMediator.maybeHandlePendingLock();
+ TestableLooper.get(this).processAllMessages();
+
+ assertTrue(mViewMediator.isShowingAndNotOccluded());
+
+ verify(mKeyguardUnlockAnimationController).notifyFinishedKeyguardExitAnimation(true);
+
+ // Unlock animators call `exitKeyguardAndFinishSurfaceBehindRemoteAnimation` when canceled
+ mViewMediator.exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false);
+ TestableLooper.get(this).processAllMessages();
+
+ verify(mUpdateMonitor, never()).dispatchKeyguardDismissAnimationFinished();
+ }
+
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ @DisableFlags(FLAG_RELOCK_WITH_POWER_BUTTON_IMMEDIATELY)
public void testCancelKeyguardExitAnimationDueToSleep_withPendingLock_keyguardWillBeShowing() {
startMockKeyguardExitAnimation();
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index e1845a17a767..e1845a17a767 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 5b216620ec2b..5b216620ec2b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
index 2ba670ceb76a..2ba670ceb76a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index 73f724e7daef..73f724e7daef 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 07e48b9da153..bf4ef509ac80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -125,6 +125,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
private static final boolean VOLUME_FIXED_TRUE = true;
private static final int LATCH_COUNT_DOWN_TIME_IN_SECOND = 5;
private static final int LATCH_TIME_OUT_TIME_IN_SECOND = 10;
+ private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic";
+ private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset";
@Mock
private DialogTransitionAnimator mDialogTransitionAnimator;
@@ -568,7 +570,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_BUILTIN_MIC);
final MediaDevice mediaDevice4 =
InputMediaDevice.create(
mContext,
@@ -576,7 +579,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_WIRED_HEADSET);
final List<MediaDevice> inputDevices = new ArrayList<>();
inputDevices.add(mediaDevice3);
inputDevices.add(mediaDevice4);
@@ -1355,7 +1359,8 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
AudioDeviceInfo.TYPE_BUILTIN_MIC,
MAX_VOLUME,
CURRENT_VOLUME,
- VOLUME_FIXED_TRUE);
+ VOLUME_FIXED_TRUE,
+ PRODUCT_NAME_BUILTIN_MIC);
mMediaSwitchingController.connectDevice(inputMediaDevice);
CountDownLatch latch = new CountDownLatch(LATCH_COUNT_DOWN_TIME_IN_SECOND);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
index fe1b963f6801..fe1b963f6801 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 8d060e936cd9..8d060e936cd9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index ee1c0e99d6ac..ee1c0e99d6ac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 963973588236..963973588236 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
index c50702868025..c50702868025 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
index 6febb91db992..7a579bacc86d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
@@ -58,7 +58,7 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest {
private static final int MIN_RSSI = -100;
private static final int MAX_RSSI = -55;
private WifiInfo mWifiInfo = mock(WifiInfo.class);
- private VcnTransportInfo mVcnTransportInfo = mock(VcnTransportInfo.class);
+ private VcnTransportInfo mVcnTransportInfo = new VcnTransportInfo.Builder().build();
@Before
public void setUp() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 59fc0d157d54..87cda64ed8e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -591,8 +591,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class);
verify(mStackScroller).setFooterView(captor.capture());
- assertNotNull(captor.getValue().findViewById(R.id.manage_text).hasOnClickListeners());
- assertNotNull(captor.getValue().findViewById(R.id.dismiss_text).hasOnClickListeners());
+ assertNotNull(captor.getValue().findViewById(R.id.manage_text));
+ assertNotNull(captor.getValue().findViewById(R.id.dismiss_text));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 4b6e31303f79..c48898aad087 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -50,8 +50,8 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
@@ -66,6 +66,7 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -104,6 +105,7 @@ import org.mockito.kotlin.whenever
// to run the callback and this makes the looper place nicely with TestScope etc.
@TestableLooper.RunWithLooper
class MobileConnectionsRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
private val flags =
FakeFeatureFlagsClassic().also { it.set(Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) }
@@ -120,13 +122,13 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: MobileInputLogger
- @Mock private lateinit var summaryLogger: TableLogBuffer
+ private val summaryLogger = logcatTableLogBuffer(kosmos, "summaryLogger")
@Mock private lateinit var logBufferFactory: TableLogBufferFactory
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var wifiManager: WifiManager
@Mock private lateinit var wifiPickerTrackerFactory: WifiPickerTrackerFactory
@Mock private lateinit var wifiPickerTracker: WifiPickerTracker
- @Mock private lateinit var wifiTableLogBuffer: TableLogBuffer
+ private val wifiTableLogBuffer = logcatTableLogBuffer(kosmos, "wifiTableLog")
private val mobileMappings = FakeMobileMappingsProxy()
private val subscriptionManagerProxy = FakeSubscriptionManagerProxy()
@@ -134,6 +136,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
private val wifiLogBuffer = LogBuffer("wifi", maxSize = 100, logcatEchoTracker = mock())
private val wifiPickerTrackerCallback =
argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
+ private val vcnTransportInfo = VcnTransportInfo.Builder().build()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -153,7 +156,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
}
whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ ->
- mock<TableLogBuffer>()
+ logcatTableLogBuffer(kosmos, "test")
}
whenever(wifiPickerTrackerFactory.create(any(), capture(wifiPickerTrackerCallback), any()))
@@ -606,10 +609,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
// WHEN an appropriate intent gets sent out
val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- intent,
- )
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
runCurrent()
// THEN the repo's state is updated despite no listeners
@@ -636,10 +636,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
// GIVEN a broadcast goes out for the appropriate subID
val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- intent,
- )
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
runCurrent()
// THEN the device is in ECM, because one of the service states is
@@ -666,10 +663,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
// GIVEN a broadcast goes out for the appropriate subID
val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- intent,
- )
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
runCurrent()
// THEN the device is in ECM, because one of the service states is
@@ -820,17 +814,9 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
// Get repos to trigger creation
underTest.getRepoForSubId(SUB_1_ID)
- verify(logBufferFactory)
- .getOrCreate(
- eq(tableBufferLogName(SUB_1_ID)),
- anyInt(),
- )
+ verify(logBufferFactory).getOrCreate(eq(tableBufferLogName(SUB_1_ID)), anyInt())
underTest.getRepoForSubId(SUB_2_ID)
- verify(logBufferFactory)
- .getOrCreate(
- eq(tableBufferLogName(SUB_2_ID)),
- anyInt(),
- )
+ verify(logBufferFactory).getOrCreate(eq(tableBufferLogName(SUB_2_ID)), anyInt())
}
@Test
@@ -1018,6 +1004,18 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
assertThat(latest).isTrue()
}
+ private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+ val network = mock<Network>()
+ val capabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(it.transportInfo).thenReturn(wifiInfo)
+ }
+ whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+ return network
+ }
+
/** Regression test for b/272586234. */
@Test
fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() =
@@ -1027,10 +1025,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
whenever(this.isCarrierMerged).thenReturn(true)
whenever(this.isPrimary).thenReturn(true)
}
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val caps =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
val latest by collectLastValue(underTest.hasCarrierMergedConnection)
@@ -1049,10 +1049,12 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
whenever(this.isCarrierMerged).thenReturn(true)
whenever(this.isPrimary).thenReturn(true)
}
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val caps =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
val latest by collectLastValue(underTest.hasCarrierMergedConnection)
@@ -1109,10 +1111,15 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
whenever(this.isCarrierMerged).thenReturn(true)
whenever(this.isPrimary).thenReturn(true)
}
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+
val underlyingCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
.thenReturn(underlyingCapabilities)
@@ -1578,9 +1585,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
* To properly mimic telephony manager, create a service state, and then turn it into an
* intent
*/
- private fun serviceStateIntent(
- subId: Int,
- ): Intent {
+ private fun serviceStateIntent(subId: Int): Intent {
return Intent(Intent.ACTION_SERVICE_STATE).apply {
putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
index 0945742fb325..88f262bec123 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
@@ -23,6 +23,7 @@ import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.TelephonyNetworkSpecifier
import android.net.VpnTransportInfo
import android.net.vcn.VcnTransportInfo
import android.net.wifi.WifiInfo
@@ -74,6 +75,8 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
private val testScope = kosmos.testScope
private val tunerService = mock<TunerService>()
+ private val vcnTransportInfo = VcnTransportInfo.Builder().build()
+
@Before
fun setUp() {
createAndSetRepo()
@@ -343,6 +346,30 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
assertThat(latest!!.wifi.isDefault).isTrue()
}
+ private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+ val network = mock<Network>()
+ val capabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(it.transportInfo).thenReturn(wifiInfo)
+ }
+ whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+ return network
+ }
+
+ private fun newCellNetwork(subId: Int): Network {
+ val network = mock<Network>()
+ val capabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(it.networkSpecifier).thenReturn(TelephonyNetworkSpecifier(subId))
+ }
+ whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+ return network
+ }
+
@Test
fun defaultConnections_carrierMergedViaWifiWithVcnTransport_wifiAndCarrierMergedDefault() =
testScope.runTest {
@@ -350,10 +377,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false)
}
@@ -373,10 +402,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false)
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false)
}
@@ -561,10 +592,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
val underlyingCarrierMergedNetwork = mock<Network>()
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val underlyingCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
.thenReturn(underlyingCapabilities)
@@ -645,14 +678,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
@Test
fun vcnSubId_tracksVcnTransportInfo() =
testScope.runTest {
- val vcnInfo = VcnTransportInfo(SUB_1_ID)
+ val underlyingCell = newCellNetwork(SUB_1_ID)
val latest by collectLastValue(underTest.vcnSubId)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell))
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -663,14 +697,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
@Test
fun vcnSubId_filersOutInvalid() =
testScope.runTest {
- val vcnInfo = VcnTransportInfo(INVALID_SUBSCRIPTION_ID)
+ val underlyingCell = newCellNetwork(INVALID_SUBSCRIPTION_ID)
val latest by collectLastValue(underTest.vcnSubId)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell))
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -703,11 +738,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
val latest by collectLastValue(underTest.vcnSubId)
val wifiInfo = mock<WifiInfo>()
- val vcnInfo = VcnTransportInfo(wifiInfo)
+ val underlyingWifi = newWifiNetwork(wifiInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -721,14 +757,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
val latest by collectLastValue(underTest.vcnSubId)
val wifiInfo = mock<WifiInfo>()
- val wifiVcnInfo = VcnTransportInfo(wifiInfo)
- val sub1VcnInfo = VcnTransportInfo(SUB_1_ID)
- val sub2VcnInfo = VcnTransportInfo(SUB_2_ID)
+ val underlyingWifi = newWifiNetwork(wifiInfo)
+ val underlyingCell1 = newCellNetwork(SUB_1_ID)
+ val underlyingCell2 = newCellNetwork(SUB_2_ID)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(wifiVcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
// WIFI VCN info
@@ -738,14 +775,16 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
// Cellular VCN info with subId 1
whenever(capabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(true)
- whenever(capabilities.transportInfo).thenReturn(sub1VcnInfo)
+ whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell1))
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
assertThat(latest).isEqualTo(SUB_1_ID)
// Cellular VCN info with subId 2
- whenever(capabilities.transportInfo).thenReturn(sub2VcnInfo)
+ whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell2))
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -776,11 +815,12 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
@Test
fun getMainOrUnderlyingWifiInfo_vcnWithWifi_hasInfo() {
val wifiInfo = mock<WifiInfo>()
- val vcnInfo = VcnTransportInfo(wifiInfo)
+ val underlyingWifi = newWifiNetwork(wifiInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
val result = capabilities.getMainOrUnderlyingWifiInfo(connectivityManager)
@@ -860,11 +900,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
fun getMainOrUnderlyingWifiInfo_cellular_underlyingVcnWithWifi_hasInfo() {
val wifiInfo = mock<WifiInfo>()
val underlyingNetwork = mock<Network>()
- val underlyingVcnInfo = VcnTransportInfo(wifiInfo)
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(wifiInfo)
+
val underlyingWifiCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(underlyingVcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
.thenReturn(underlyingWifiCapabilities)
@@ -887,11 +931,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
@DisableFlags(FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS)
fun getMainOrUnderlyingWifiInfo_notCellular_underlyingVcnWithWifi_noInfo() {
val underlyingNetwork = mock<Network>()
- val underlyingVcnInfo = VcnTransportInfo(mock<WifiInfo>())
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(mock<WifiInfo>())
+
val underlyingWifiCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(underlyingVcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
.thenReturn(underlyingWifiCapabilities)
@@ -917,10 +965,15 @@ class ConnectivityRepositoryImplTest : SysuiTestCase() {
val underlyingCarrierMergedNetwork = mock<Network>()
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+
val underlyingCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
.thenReturn(underlyingCapabilities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index fd4b77d7fb95..44e1437b909e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -26,19 +26,20 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.util.time.fakeSystemClock
import com.android.wifitrackerlib.HotspotNetworkEntry
import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType
import com.android.wifitrackerlib.MergedCarrierEntry
@@ -67,6 +68,7 @@ import org.mockito.Mockito.verify
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class WifiRepositoryImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
// Using lazy means that the class will only be constructed once it's fetched. Because the
// repository internally sets some values on construction, we need to set up some test
@@ -84,9 +86,9 @@ class WifiRepositoryImplTest : SysuiTestCase() {
)
}
- private val executor = FakeExecutor(FakeSystemClock())
+ private val executor = FakeExecutor(kosmos.fakeSystemClock)
private val logger = LogBuffer("name", maxSize = 100, logcatEchoTracker = mock())
- private val tableLogger = mock<TableLogBuffer>()
+ private val tableLogger = logcatTableLogBuffer(kosmos, "WifiRepositoryImplTest")
private val wifiManager =
mock<WifiManager>().apply { whenever(this.maxSignalLevel).thenReturn(10) }
private val wifiPickerTrackerFactory = mock<WifiPickerTrackerFactory>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
new file mode 100644
index 000000000000..faaa4c415d28
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.app.viewcapture.mockViewCaptureAwareWindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val testScope = kosmos.testScope
+ private val fakeDisplayRepository = kosmos.displayRepository
+
+ private val store =
+ MultiDisplayStatusBarWindowControllerStore(
+ backgroundApplicationScope = kosmos.applicationCoroutineScope,
+ controllerFactory = kosmos.fakeStatusBarWindowControllerFactory,
+ displayWindowPropertiesRepository = kosmos.fakeDisplayWindowPropertiesRepository,
+ viewCaptureAwareWindowManagerFactory =
+ object : ViewCaptureAwareWindowManager.Factory {
+ override fun create(
+ windowManager: WindowManager
+ ): ViewCaptureAwareWindowManager {
+ return kosmos.mockViewCaptureAwareWindowManager
+ }
+ },
+ displayRepository = fakeDisplayRepository,
+ )
+
+ @Before
+ fun start() {
+ store.start()
+ }
+
+ @Before
+ fun addDisplays() = runBlocking {
+ fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+ }
+
+ @Test
+ fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() =
+ testScope.runTest {
+ val controller = store.defaultDisplay
+
+ assertThat(store.defaultDisplay).isSameInstanceAs(controller)
+ }
+
+ @Test
+ fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() =
+ testScope.runTest {
+ val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller)
+ }
+
+ @Test
+ fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() =
+ testScope.runTest {
+ val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+
+ assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun forDisplay_nonExistingDisplayId_throws() =
+ testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) }
+
+ private fun createDisplay(displayId: Int): Display = mock {
+ on { getDisplayId() } doReturn displayId
+ }
+
+ companion object {
+ private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
+ private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
+ private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt
new file mode 100644
index 000000000000..e1c6699348a9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.app.viewcapture
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockViewCaptureAwareWindowManager by
+ Kosmos.Fixture { mock<ViewCaptureAwareWindowManager>() }
+
+var Kosmos.viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager by
+ Kosmos.Fixture { mockViewCaptureAwareWindowManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt
new file mode 100644
index 000000000000..cac4ff3fe52b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.audioSharingButtonViewModel: AudioSharingButtonViewModel by
+ Kosmos.Fixture {
+ AudioSharingButtonViewModel(
+ localBluetoothManager,
+ audioSharingInteractor,
+ bluetoothStateInteractor,
+ deviceItemInteractor,
+ )
+ }
+
+val Kosmos.audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory by
+ Kosmos.Fixture {
+ object : AudioSharingButtonViewModel.Factory {
+ override fun create(): AudioSharingButtonViewModel {
+ return audioSharingButtonViewModel
+ }
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt
new file mode 100644
index 000000000000..8019efc50391
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.plugins.activityStarter
+
+val Kosmos.audioSharingDeviceItemActionInteractorImpl: AudioSharingDeviceItemActionInteractorImpl by
+ Kosmos.Fixture {
+ AudioSharingDeviceItemActionInteractorImpl(
+ activityStarter,
+ audioSharingInteractor,
+ dialogTransitionAnimator,
+ localBluetoothManager,
+ testDispatcher,
+ testDispatcher,
+ bluetoothTileDialogLogger,
+ uiEventLogger,
+ audioSharingDialogDelegateFactory,
+ deviceItemActionInteractorImpl,
+ )
+ }
+
+val Kosmos.audioSharingDialogDelegateFactory: AudioSharingDialogDelegate.Factory by
+ Kosmos.Fixture {
+ object : AudioSharingDialogDelegate.Factory {
+ override fun create(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): AudioSharingDialogDelegate {
+ return audioSharingDialogDelegate
+ }
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt
new file mode 100644
index 000000000000..b8899de8fdc7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import android.content.applicationContext
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
+import kotlinx.coroutines.CoroutineScope
+import org.mockito.kotlin.mock
+
+val Kosmos.cachedBluetoothDevice: CachedBluetoothDevice by Kosmos.Fixture { mock {} }
+
+val Kosmos.audioSharingDialogViewModel: AudioSharingDialogViewModel by
+ Kosmos.Fixture {
+ AudioSharingDialogViewModel(
+ deviceItemInteractor,
+ audioSharingInteractor,
+ applicationContext,
+ localBluetoothManager,
+ cachedBluetoothDevice,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+ }
+
+val Kosmos.audioSharingDialogViewModelFactory: AudioSharingDialogViewModel.Factory by
+ Kosmos.Fixture {
+ object : AudioSharingDialogViewModel.Factory {
+ override fun create(
+ cachedBluetoothDevice: CachedBluetoothDevice,
+ coroutineScope: CoroutineScope
+ ): AudioSharingDialogViewModel {
+ return audioSharingDialogViewModel
+ }
+ }
+ }
+
+val Kosmos.audioSharingDialogDelegate: AudioSharingDialogDelegate by
+ Kosmos.Fixture {
+ AudioSharingDialogDelegate(
+ cachedBluetoothDevice,
+ testScope.backgroundScope,
+ audioSharingDialogViewModelFactory,
+ systemUIDialogDotFactory,
+ uiEventLogger
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt
new file mode 100644
index 000000000000..4f4d1da42303
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.audioSharingInteractor: AudioSharingInteractor by
+ Kosmos.Fixture {
+ AudioSharingInteractorImpl(
+ localBluetoothManager,
+ bluetoothTileDialogAudioSharingRepository,
+ testDispatcher,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt
new file mode 100644
index 000000000000..d15d0e5ac7ff
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.bluetoothTileDialogAudioSharingRepository by
+ Kosmos.Fixture { FakeAudioSharingRepository() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
index 969e26a8d884..969e26a8d884 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt
new file mode 100644
index 000000000000..aaa918c9ff35
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+
+val Kosmos.bluetoothStateInteractor: BluetoothStateInteractor by
+ Kosmos.Fixture {
+ BluetoothStateInteractor(
+ localBluetoothManager,
+ bluetoothTileDialogLogger,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+ }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
index 5ff46346b386..b5b2f5e3e802 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
@@ -20,8 +20,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.util.mockito.mock
+import org.mockito.kotlin.mock
val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} }
@@ -29,14 +28,10 @@ val Kosmos.localBluetoothManager: LocalBluetoothManager by Kosmos.Fixture { mock
val Kosmos.dialogTransitionAnimator: DialogTransitionAnimator by Kosmos.Fixture { mock {} }
-val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by
+val Kosmos.deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl by
Kosmos.Fixture {
- DeviceItemActionInteractor(
- activityStarter,
- dialogTransitionAnimator,
- localBluetoothManager,
+ DeviceItemActionInteractorImpl(
testDispatcher,
- bluetoothTileDialogLogger,
uiEventLogger,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
new file mode 100644
index 000000000000..a839f17aad82
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeAudioSharingRepository : AudioSharingRepository {
+ private var mutableAvailable: Boolean = false
+
+ private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ private val mutableAudioSourceStateUpdate = MutableSharedFlow<Unit>()
+
+ var sourceAdded: Boolean = false
+ private set
+
+ private var profile: LocalBluetoothLeBroadcast? = null
+
+ override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+ get() = profile
+
+ override val audioSourceStateUpdate: Flow<Unit> = mutableAudioSourceStateUpdate
+
+ override val inAudioSharing: StateFlow<Boolean> = mutableInAudioSharing
+
+ override suspend fun audioSharingAvailable(): Boolean = mutableAvailable
+
+ override suspend fun addSource() {
+ sourceAdded = true
+ }
+
+ override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+ override suspend fun startAudioSharing() {}
+
+ fun setAudioSharingAvailable(available: Boolean) {
+ mutableAvailable = available
+ }
+
+ fun setInAudioSharing(state: Boolean) {
+ mutableInAudioSharing.value = state
+ }
+
+ fun setLeAudioBroadcastProfile(leAudioBroadcastProfile: LocalBluetoothLeBroadcast?) {
+ profile = leAudioBroadcastProfile
+ }
+
+ fun emitAudioSourceStateUpdate() {
+ mutableAudioSourceStateUpdate.tryEmit(Unit)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
index 0e8427310895..e470e37ade0a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
@@ -19,14 +19,13 @@ package com.android.systemui.brightness.domain.interactor
import com.android.systemui.brightness.data.repository.screenBrightnessRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.log.table.logcatTableLogBuffer
val Kosmos.screenBrightnessInteractor by
Kosmos.Fixture {
ScreenBrightnessInteractor(
screenBrightnessRepository,
applicationCoroutineScope,
- mock<TableLogBuffer>(),
+ logcatTableLogBuffer(this, "screenBrightness"),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt
new file mode 100644
index 000000000000..ff4ba61b6965
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.fakeDisplayScopeRepository by
+ Kosmos.Fixture { FakeDisplayScopeRepository(testDispatcher) }
+
+var Kosmos.displayScopeRepository: DisplayScopeRepository by
+ Kosmos.Fixture { fakeDisplayScopeRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt
new file mode 100644
index 000000000000..65b18c102a16
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeDisplayWindowPropertiesRepository by
+ Kosmos.Fixture { FakeDisplayWindowPropertiesRepository() }
+
+var Kosmos.displayWindowPropertiesRepository: DisplayWindowPropertiesRepository by
+ Kosmos.Fixture { fakeDisplayWindowPropertiesRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt
new file mode 100644
index 000000000000..3c2592471694
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+
+class FakeDisplayScopeRepository(private val dispatcher: CoroutineDispatcher) :
+ DisplayScopeRepository {
+
+ private val perDisplayScopes = mutableMapOf<Int, CoroutineScope>()
+
+ override fun scopeForDisplay(displayId: Int): CoroutineScope {
+ return perDisplayScopes.computeIfAbsent(displayId) { CoroutineScope(dispatcher) }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt
new file mode 100644
index 000000000000..9282f275b20b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.google.common.collect.HashBasedTable
+import org.mockito.kotlin.mock
+
+class FakeDisplayWindowPropertiesRepository : DisplayWindowPropertiesRepository {
+
+ private val properties = HashBasedTable.create<Int, Int, DisplayWindowProperties>()
+
+ override fun get(displayId: Int, windowType: Int): DisplayWindowProperties {
+ return properties.get(displayId, windowType)
+ ?: DisplayWindowProperties(
+ displayId = displayId,
+ windowType = windowType,
+ context = mock(),
+ windowManager = mock(),
+ )
+ .also { properties.put(displayId, windowType, it) }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index f97f30383398..522c387a0b08 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -23,6 +23,7 @@ import android.os.fakeExecutorHandler
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.bouncerRepository
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
import com.android.systemui.classifier.falsingCollector
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
@@ -152,6 +153,7 @@ class KosmosJavaAdapter() {
val wifiInteractor by lazy { kosmos.wifiInteractor }
val fakeWifiRepository by lazy { kosmos.fakeWifiRepository }
val volumeDialogInteractor by lazy { kosmos.volumeDialogInteractor }
+ val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor }
val ongoingActivityChipsViewModel by lazy { kosmos.ongoingActivityChipsViewModel }
val scrimController by lazy { kosmos.scrimController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index cfc31c7f301c..10b073e8f331 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.plugins.statusbar
import com.android.internal.logging.uiEventLogger
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
@@ -45,5 +46,6 @@ var Kosmos.statusBarStateController: SysuiStatusBarStateController by
{ sceneContainerOcclusionInteractor },
{ keyguardClockInteractor },
{ sceneBackInteractor },
+ { alternateBouncerInteractor },
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 237f7e4c4dc8..a25a3c0c2044 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -49,6 +49,7 @@ import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
@@ -92,6 +93,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture {
primaryBouncerToLockscreenTransitionViewModel,
aodBurnInViewModel = aodBurnInViewModel,
communalSceneInteractor = communalSceneInteractor,
+ headsUpNotificationInteractor = { headsUpNotificationInteractor },
unfoldTransitionInteractor = unfoldTransitionInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
index 99cd8309631e..68d08e285c53 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorKosmos.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.policy.domain.interactor
-import android.content.testableContext
+import android.content.applicationContext
import com.android.settingslib.notification.modes.zenIconLoader
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -28,7 +28,7 @@ import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
val Kosmos.zenModeInteractor by Fixture {
ZenModeInteractor(
- context = testableContext,
+ context = applicationContext,
zenModeRepository = zenModeRepository,
notificationSettingsRepository = notificationSettingsRepository,
bgDispatcher = testDispatcher,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
new file mode 100644
index 000000000000..10f328be12d2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.content.Context
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+
+class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory {
+ override fun create(
+ context: Context,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
+ ) = FakeStatusBarWindowController()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt
new file mode 100644
index 000000000000..d19e3227027c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.view.Display
+
+class FakeStatusBarWindowControllerStore : StatusBarWindowControllerStore {
+
+ private val perDisplayControllers = mutableMapOf<Int, FakeStatusBarWindowController>()
+
+ override val defaultDisplay
+ get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+ override fun forDisplay(displayId: Int): StatusBarWindowController {
+ return perDisplayControllers.computeIfAbsent(displayId) { FakeStatusBarWindowController() }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
index c198b35be289..6c6f243f3953 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -21,3 +21,15 @@ import com.android.systemui.kosmos.Kosmos
val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() }
var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController }
+
+val Kosmos.fakeStatusBarWindowControllerStore by
+ Kosmos.Fixture { FakeStatusBarWindowControllerStore() }
+
+var Kosmos.statusBarWindowControllerStore: StatusBarWindowControllerStore by
+ Kosmos.Fixture { fakeStatusBarWindowControllerStore }
+
+val Kosmos.fakeStatusBarWindowControllerFactory by
+ Kosmos.Fixture { FakeStatusBarWindowControllerFactory() }
+
+var Kosmos.statusBarWindowControllerFactory: StatusBarWindowController.Factory by
+ Kosmos.Fixture { fakeStatusBarWindowControllerFactory }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
index a4719e5a2492..5da6ee95234c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeAudioSharingRepository : AudioSharingRepository {
+ private var mutableAvailable: Boolean = false
private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val mutablePrimaryGroupId: MutableStateFlow<Int> =
MutableStateFlow(TEST_GROUP_ID_INVALID)
@@ -34,8 +35,14 @@ class FakeAudioSharingRepository : AudioSharingRepository {
override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId
override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap
+ override suspend fun audioSharingAvailable(): Boolean = mutableAvailable
+
override suspend fun setSecondaryVolume(volume: Int) {}
+ fun setAudioSharingAvailable(available: Boolean) {
+ mutableAvailable = available
+ }
+
fun setInAudioSharing(state: Boolean) {
mutableInAudioSharing.value = state
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt
new file mode 100644
index 000000000000..291dfc0430e2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.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.volume.dialog.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository
+
+val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
index e73539eac6f1..7376c7fb1495 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -18,8 +18,15 @@ package com.android.systemui.volume.dialog.domain.interactor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.utils.volumeTracer
val Kosmos.volumeDialogVisibilityInteractor by
Kosmos.Fixture {
- VolumeDialogVisibilityInteractor(applicationCoroutineScope, volumeDialogCallbacksInteractor)
+ VolumeDialogVisibilityInteractor(
+ applicationCoroutineScope,
+ volumeDialogCallbacksInteractor,
+ volumeTracer,
+ volumeDialogVisibilityRepository,
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt
new file mode 100644
index 000000000000..a5074ebe0119
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/FakeVolumeTracer.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.utils
+
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+
+class FakeVolumeTracer : VolumeTracer {
+
+ override fun traceVisibilityStart(model: VolumeDialogVisibilityModel) {}
+
+ override fun traceVisibilityEnd(model: VolumeDialogVisibilityModel) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.kt
new file mode 100644
index 000000000000..138256330b2b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/utils/VolumeTracerKosmos.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.volume.dialog.utils
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeVolumeTracer: FakeVolumeTracer by Kosmos.Fixture { FakeVolumeTracer() }
+var Kosmos.volumeTracer: VolumeTracer by Kosmos.Fixture { fakeVolumeTracer }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
index 63a132565177..db66c3eefb52 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+import android.content.mockedContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.volume.domain.interactor.audioModeInteractor
@@ -27,6 +28,7 @@ import com.android.systemui.volume.mediaOutputInteractor
val Kosmos.mediaOutputComponentInteractor by
Kosmos.Fixture {
MediaOutputComponentInteractor(
+ mockedContext,
testScope.backgroundScope,
mediaDeviceSessionInteractor,
audioOutputInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt
index e6b52f0c52e0..55f0a28d0135 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt
@@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
import android.content.applicationContext
import com.android.internal.logging.uiEventLogger
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
import com.android.systemui.volume.shared.volumePanelLogger
import kotlinx.coroutines.CoroutineScope
@@ -36,6 +37,7 @@ val Kosmos.audioStreamSliderViewModelFactory by
coroutineScope,
applicationContext,
audioVolumeInteractor,
+ zenModeInteractor,
uiEventLogger,
volumePanelLogger,
)
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 11b66fc3f1e5..9629a87c1057 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -154,6 +154,8 @@ java_library {
"framework-annotations-lib",
"ravenwood-helper-framework-runtime",
"ravenwood-helper-libcore-runtime",
+ "hoststubgen-helper-runtime.ravenwood",
+ "mockito-ravenwood-prebuilt",
],
visibility: ["//frameworks/base"],
jarjar_rules: ":ravenwood-services-jarjar-rules",
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 644babb8984d..908e5903122e 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -22,10 +22,14 @@ import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOS
import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.ResourcesManager;
+import android.app.UiAutomation;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Build;
@@ -40,6 +44,7 @@ import android.util.Log;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.hoststubgen.hosthelper.HostTestUtils;
import com.android.internal.os.RuntimeInit;
import com.android.ravenwood.RavenwoodRuntimeNative;
import com.android.ravenwood.common.RavenwoodCommonUtils;
@@ -52,8 +57,10 @@ import org.junit.runner.Description;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
+import java.util.Collections;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -125,6 +132,9 @@ public class RavenwoodRuntimeEnvironmentController {
private static RavenwoodConfig sConfig;
private static RavenwoodSystemProperties sProps;
+ // TODO: use the real UiAutomation class instead of a mock
+ private static UiAutomation sMockUiAutomation;
+ private static Set<String> sAdoptedPermissions = Collections.emptySet();
private static boolean sInitialized = false;
/**
@@ -171,6 +181,7 @@ public class RavenwoodRuntimeEnvironmentController {
"androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
assertMockitoVersion();
+ sMockUiAutomation = createMockUiAutomation();
}
/**
@@ -261,7 +272,7 @@ public class RavenwoodRuntimeEnvironmentController {
// Prepare other fields.
config.mInstrumentation = new Instrumentation();
- config.mInstrumentation.basicInit(config.mInstContext, config.mTargetContext);
+ config.mInstrumentation.basicInit(instContext, targetContext, sMockUiAutomation);
InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY);
RavenwoodSystemServer.init(config);
@@ -300,12 +311,13 @@ public class RavenwoodRuntimeEnvironmentController {
config.mInstrumentation = null;
if (config.mInstContext != null) {
((RavenwoodContext) config.mInstContext).cleanUp();
+ config.mInstContext = null;
}
if (config.mTargetContext != null) {
((RavenwoodContext) config.mTargetContext).cleanUp();
+ config.mTargetContext = null;
}
- config.mInstContext = null;
- config.mTargetContext = null;
+ sMockUiAutomation.dropShellPermissionIdentity();
if (config.mProvideMainThread) {
Looper.getMainLooper().quit();
@@ -403,6 +415,31 @@ public class RavenwoodRuntimeEnvironmentController {
() -> Class.forName("org.mockito.Matchers"));
}
+ private static UiAutomation createMockUiAutomation() {
+ var mock = mock(UiAutomation.class, inv -> {
+ HostTestUtils.onThrowMethodCalled();
+ return null;
+ });
+ doAnswer(inv -> {
+ sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+ return null;
+ }).when(mock).adoptShellPermissionIdentity();
+ doAnswer(inv -> {
+ if (inv.getArgument(0) == null) {
+ sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+ } else {
+ sAdoptedPermissions = (Set) Set.of(inv.getArguments());
+ }
+ return null;
+ }).when(mock).adoptShellPermissionIdentity(any());
+ doAnswer(inv -> {
+ sAdoptedPermissions = Collections.emptySet();
+ return null;
+ }).when(mock).dropShellPermissionIdentity();
+ doAnswer(inv -> sAdoptedPermissions).when(mock).getAdoptedShellPermissions();
+ return mock;
+ }
+
@SuppressWarnings("unused") // Called from native code (ravenwood_sysprop.cpp)
private static void checkSystemPropertyAccess(String key, boolean write) {
boolean result = write ? sProps.isKeyWritable(key) : sProps.isKeyReadable(key);
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java
new file mode 100644
index 000000000000..eb948279109b
--- /dev/null
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.ravenwoodtest.bivalenttest;
+
+import static android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG;
+import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodUiAutomationTest {
+
+ private Instrumentation mInstrumentation;
+
+ @Before
+ public void setup() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ @Test
+ public void testGetUiAutomation() {
+ assertNotNull(mInstrumentation.getUiAutomation());
+ }
+
+ @Test
+ public void testGetUiAutomationWithFlags() {
+ assertNotNull(mInstrumentation.getUiAutomation(UiAutomation.FLAG_DONT_USE_ACCESSIBILITY));
+ }
+
+ @Test
+ public void testShellPermissionApis() {
+ var uiAutomation = mInstrumentation.getUiAutomation();
+ assertTrue(uiAutomation.getAdoptedShellPermissions().isEmpty());
+ uiAutomation.adoptShellPermissionIdentity();
+ assertEquals(uiAutomation.getAdoptedShellPermissions(), UiAutomation.ALL_PERMISSIONS);
+ uiAutomation.adoptShellPermissionIdentity((String[]) null);
+ assertEquals(uiAutomation.getAdoptedShellPermissions(), UiAutomation.ALL_PERMISSIONS);
+ uiAutomation.adoptShellPermissionIdentity(
+ OVERRIDE_COMPAT_CHANGE_CONFIG, READ_COMPAT_CHANGE_CONFIG);
+ assertEquals(uiAutomation.getAdoptedShellPermissions(),
+ Set.of(OVERRIDE_COMPAT_CHANGE_CONFIG, READ_COMPAT_CHANGE_CONFIG));
+ uiAutomation.dropShellPermissionIdentity();
+ assertTrue(uiAutomation.getAdoptedShellPermissions().isEmpty());
+ }
+
+ @Test
+ public void testUnsupportedMethod() {
+ // Only unsupported on Ravenwood
+ assumeTrue(RavenwoodCommonUtils.isOnRavenwood());
+ assertThrows(RuntimeException.class,
+ () -> mInstrumentation.getUiAutomation().executeShellCommand("echo ok"));
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index d595d02016e0..1451dfaa7964 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -3653,6 +3653,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
return;
}
+ // Magnification connection should not be requested for visible background users.
+ // (b/332222893)
+ if (mUmi.isVisibleBackgroundFullUser(userState.mUserId)) {
+ return;
+ }
+
final boolean shortcutEnabled = (userState.isShortcutMagnificationEnabledLocked()
|| userState.isMagnificationSingleFingerTripleTapEnabledLocked()
|| (Flags.enableMagnificationMultipleFingerMultipleTapGesture()
diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
index 4b97745b3b25..1df5d1a1d54f 100644
--- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
+++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
@@ -138,8 +138,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
DIAGONAL_UP_LEFT_MOVE(KeyEvent.KEYCODE_7),
UP_MOVE_OR_SCROLL(KeyEvent.KEYCODE_8),
DIAGONAL_UP_RIGHT_MOVE(KeyEvent.KEYCODE_9),
- LEFT_MOVE(KeyEvent.KEYCODE_U),
- RIGHT_MOVE(KeyEvent.KEYCODE_O),
+ LEFT_MOVE_OR_SCROLL(KeyEvent.KEYCODE_U),
+ RIGHT_MOVE_OR_SCROLL(KeyEvent.KEYCODE_O),
DIAGONAL_DOWN_LEFT_MOVE(KeyEvent.KEYCODE_J),
DOWN_MOVE_OR_SCROLL(KeyEvent.KEYCODE_K),
DIAGONAL_DOWN_RIGHT_MOVE(KeyEvent.KEYCODE_L),
@@ -267,6 +267,16 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
);
}
+ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ private void sendVirtualMouseScrollEvent(float x, float y) {
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
+ .setXAxisMovement(x)
+ .setYAxisMovement(y)
+ .build()
+ );
+ }
+
/**
* Performs a mouse scroll action based on the provided key code.
* The scroll action will only be performed if the scroll toggle is on.
@@ -284,19 +294,31 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
private void performMouseScrollAction(int keyCode) {
MouseKeyEvent mouseKeyEvent = MouseKeyEvent.from(
keyCode, mActiveInputDeviceId, mDeviceKeyCodeMap);
- float y = switch (mouseKeyEvent) {
- case UP_MOVE_OR_SCROLL -> MOUSE_SCROLL_STEP;
- case DOWN_MOVE_OR_SCROLL -> -MOUSE_SCROLL_STEP;
- default -> 0.0f;
- };
- waitForVirtualMouseCreation();
- mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
- .setYAxisMovement(y)
- .build()
- );
+ float x = 0f;
+ float y = 0f;
+
+ switch (mouseKeyEvent) {
+ case UP_MOVE_OR_SCROLL -> {
+ y = MOUSE_SCROLL_STEP;
+ }
+ case DOWN_MOVE_OR_SCROLL -> {
+ y = -MOUSE_SCROLL_STEP;
+ }
+ case LEFT_MOVE_OR_SCROLL -> {
+ x = MOUSE_SCROLL_STEP;
+ }
+ case RIGHT_MOVE_OR_SCROLL -> {
+ x = -MOUSE_SCROLL_STEP;
+ }
+ default -> {
+ x = 0.0f;
+ y = 0.0f;
+ }
+ }
+ sendVirtualMouseScrollEvent(x, y);
if (DEBUG) {
Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
- + " for scroll action with axis movement (y=" + y + ")");
+ + " for scroll action with axis movement (x=" + x + ", y=" + y + ")");
}
}
@@ -344,8 +366,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
* The method calculates the relative movement of the mouse pointer
* and sends the corresponding event to the virtual mouse.
*
- * The UP and DOWN pointer actions will only take place for their respective keys
- * if the scroll toggle is off.
+ * The UP, DOWN, LEFT, RIGHT pointer actions will only take place for their
+ * respective keys if the scroll toggle is off.
*
* @param keyCode The key code representing the direction or button press.
* Supported keys are:
@@ -353,8 +375,8 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_LEFT_MOVE}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DOWN_MOVE_OR_SCROLL}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_DOWN_RIGHT_MOVE}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE}
- * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#LEFT_MOVE_OR_SCROLL}
+ * <li>{@link MouseKeysInterceptor.MouseKeyEvent#RIGHT_MOVE_OR_SCROLL}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_LEFT_MOVE}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#UP_MOVE_OR_SCROLL}
* <li>{@link MouseKeysInterceptor.MouseKeyEvent#DIAGONAL_UP_RIGHT_MOVE}
@@ -381,10 +403,10 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
x = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
y = MOUSE_POINTER_MOVEMENT_STEP / sqrt(2);
}
- case LEFT_MOVE -> {
+ case LEFT_MOVE_OR_SCROLL -> {
x = -MOUSE_POINTER_MOVEMENT_STEP;
}
- case RIGHT_MOVE -> {
+ case RIGHT_MOVE_OR_SCROLL -> {
x = MOUSE_POINTER_MOVEMENT_STEP;
}
case DIAGONAL_UP_LEFT_MOVE -> {
@@ -424,7 +446,9 @@ public class MouseKeysInterceptor extends BaseEventStreamTransformation
private boolean isMouseScrollKey(int keyCode, InputDevice inputDevice) {
return keyCode == MouseKeyEvent.UP_MOVE_OR_SCROLL.getKeyCode(inputDevice)
- || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCode(inputDevice);
+ || keyCode == MouseKeyEvent.DOWN_MOVE_OR_SCROLL.getKeyCode(inputDevice)
+ || keyCode == MouseKeyEvent.LEFT_MOVE_OR_SCROLL.getKeyCode(inputDevice)
+ || keyCode == MouseKeyEvent.RIGHT_MOVE_OR_SCROLL.getKeyCode(inputDevice);
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
index 19e3e690924e..fe06406e580a 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
@@ -19,6 +19,7 @@ package com.android.server.accessibility.magnification;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_MAGNIFICATION_CONNECTION;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_MAGNIFICATION_CONNECTION_CALLBACK;
import static android.os.Build.HW_TIMEOUT_MULTIPLIER;
+import static android.os.UserHandle.getCallingUserId;
import static android.view.accessibility.MagnificationAnimationCallback.STUB_ANIMATION_CALLBACK;
import static com.android.server.accessibility.AccessibilityManagerService.INVALID_SERVICE_ID;
@@ -54,6 +55,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.wm.WindowManagerInternal;
@@ -209,6 +211,7 @@ public class MagnificationConnectionManager implements
private final Callback mCallback;
private final AccessibilityTraceManager mTrace;
private final MagnificationScaleProvider mScaleProvider;
+ private final UserManagerInternal mUserManagerInternal;
public MagnificationConnectionManager(Context context, Object lock, @NonNull Callback callback,
AccessibilityTraceManager trace, MagnificationScaleProvider scaleProvider) {
@@ -217,6 +220,7 @@ public class MagnificationConnectionManager implements
mCallback = callback;
mTrace = trace;
mScaleProvider = scaleProvider;
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
}
/**
@@ -280,12 +284,18 @@ public class MagnificationConnectionManager implements
* Requests {@link IMagnificationConnection} through
* {@link StatusBarManagerInternal#requestMagnificationConnection(boolean)} and
* destroys all window magnifications if necessary.
+ * NOTE: Currently, this is not allowed to call from visible background users.(b/332222893)
*
* @param connect {@code true} if needs connection, otherwise set the connection to null and
* destroy all window magnifications.
* @return {@code true} if {@link IMagnificationConnection} state is going to change.
*/
public boolean requestConnection(boolean connect) {
+ final int callingUserId = getCallingUserId();
+ if (mUserManagerInternal.isVisibleBackgroundFullUser(callingUserId)) {
+ throw new SecurityException("Visible background user(u" + callingUserId
+ + " is not permitted to request magnification connection.");
+ }
if (DBG) {
Slog.d(TAG, "requestConnection :" + connect);
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java
new file mode 100644
index 000000000000..a832545475bf
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionDumpHelper.java
@@ -0,0 +1,188 @@
+/*
+ * 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.server.appfunctions;
+
+import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID;
+import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_INDEXER_PACKAGE;
+import static android.app.appfunctions.AppFunctionStaticMetadataHelper.PROPERTY_FUNCTION_ID;
+
+import android.Manifest;
+import android.annotation.BinderThread;
+import android.annotation.RequiresPermission;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.UserManager;
+import android.util.IndentingPrintWriter;
+import android.app.appfunctions.AppFunctionRuntimeMetadata;
+import android.app.appfunctions.AppFunctionStaticMetadataHelper;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchManager.SearchContext;
+import android.app.appsearch.JoinSpec;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+public final class AppFunctionDumpHelper {
+ private static final String TAG = AppFunctionDumpHelper.class.getSimpleName();
+
+ private AppFunctionDumpHelper() {}
+
+ /** Dumps the state of all app functions for all users. */
+ @BinderThread
+ @RequiresPermission(
+ anyOf = {Manifest.permission.CREATE_USERS, Manifest.permission.MANAGE_USERS})
+ public static void dumpAppFunctionsState(@NonNull Context context, @NonNull PrintWriter w) {
+ UserManager userManager = context.getSystemService(UserManager.class);
+ if (userManager == null) {
+ w.println("Couldn't retrieve UserManager.");
+ return;
+ }
+
+ IndentingPrintWriter pw = new IndentingPrintWriter(w);
+
+ List<UserInfo> userInfos = userManager.getAliveUsers();
+ for (UserInfo userInfo : userInfos) {
+ pw.println(
+ "AppFunction state for user " + userInfo.getUserHandle().getIdentifier() + ":");
+ pw.increaseIndent();
+ dumpAppFunctionsStateForUser(
+ context.createContextAsUser(userInfo.getUserHandle(), /* flags= */ 0), pw);
+ pw.decreaseIndent();
+ }
+ }
+
+ private static void dumpAppFunctionsStateForUser(
+ @NonNull Context context, @NonNull IndentingPrintWriter pw) {
+ AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class);
+ if (appSearchManager == null) {
+ pw.println("Couldn't retrieve AppSearchManager.");
+ return;
+ }
+
+ try (FutureGlobalSearchSession searchSession =
+ new FutureGlobalSearchSession(appSearchManager, Runnable::run)) {
+ pw.println();
+
+ try (FutureSearchResults futureSearchResults =
+ searchSession.search("", buildAppFunctionMetadataSearchSpec()).get(); ) {
+ List<SearchResult> searchResultsList;
+ do {
+ searchResultsList = futureSearchResults.getNextPage().get();
+ for (SearchResult searchResult : searchResultsList) {
+ dumpAppFunctionMetadata(pw, searchResult);
+ }
+ } while (!searchResultsList.isEmpty());
+ }
+
+ } catch (Exception e) {
+ pw.println("Failed to dump AppFunction state: " + e);
+ }
+ }
+
+ private static SearchSpec buildAppFunctionMetadataSearchSpec() {
+ SearchSpec runtimeMetadataSearchSpec =
+ new SearchSpec.Builder()
+ .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
+ .addFilterSchemas(AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE)
+ .build();
+ JoinSpec joinSpec =
+ new JoinSpec.Builder(PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID)
+ .setNestedSearch(/* queryExpression= */ "", runtimeMetadataSearchSpec)
+ .build();
+
+ return new SearchSpec.Builder()
+ .addFilterPackageNames(APP_FUNCTION_INDEXER_PACKAGE)
+ .addFilterSchemas(AppFunctionStaticMetadataHelper.STATIC_SCHEMA_TYPE)
+ .setJoinSpec(joinSpec)
+ .build();
+ }
+
+ private static void dumpAppFunctionMetadata(
+ IndentingPrintWriter pw, SearchResult joinedSearchResult) {
+ pw.println(
+ "AppFunctionMetadata for: "
+ + joinedSearchResult
+ .getGenericDocument()
+ .getPropertyString(PROPERTY_FUNCTION_ID));
+ pw.increaseIndent();
+
+ pw.println("Static Metadata:");
+ pw.increaseIndent();
+ writeGenericDocumentProperties(pw, joinedSearchResult.getGenericDocument());
+ pw.decreaseIndent();
+
+ pw.println("Runtime Metadata:");
+ pw.increaseIndent();
+ if (!joinedSearchResult.getJoinedResults().isEmpty()) {
+ writeGenericDocumentProperties(
+ pw, joinedSearchResult.getJoinedResults().getFirst().getGenericDocument());
+ } else {
+ pw.println("No runtime metadata found.");
+ }
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+
+ private static void writeGenericDocumentProperties(
+ IndentingPrintWriter pw, GenericDocument genericDocument) {
+ Set<String> propertyNames = genericDocument.getPropertyNames();
+ pw.println("{");
+ pw.increaseIndent();
+ for (String propertyName : propertyNames) {
+ Object propertyValue = genericDocument.getProperty(propertyName);
+ pw.print("\"" + propertyName + "\"" + ": [");
+
+ if (propertyValue instanceof GenericDocument[]) {
+ GenericDocument[] documentValues = (GenericDocument[]) propertyValue;
+ for (int i = 0; i < documentValues.length; i++) {
+ GenericDocument documentValue = documentValues[i];
+ writeGenericDocumentProperties(pw, documentValue);
+ if (i != documentValues.length - 1) {
+ pw.print(", ");
+ }
+ pw.println();
+ }
+ } else {
+ int propertyArrLength = Array.getLength(propertyValue);
+ for (int i = 0; i < propertyArrLength; i++) {
+ Object propertyElement = Array.get(propertyValue, i);
+ if (propertyElement instanceof String) {
+ pw.print("\"" + propertyElement + "\"");
+ } else if (propertyElement instanceof byte[]) {
+ pw.print(Arrays.toString((byte[]) propertyElement));
+ } else if (propertyElement != null) {
+ pw.print(propertyElement.toString());
+ }
+ if (i != propertyArrLength - 1) {
+ pw.print(", ");
+ }
+ }
+ }
+ pw.println("]");
+ }
+ pw.decreaseIndent();
+ pw.println("}");
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index d31ced3f2ce3..5d574083b326 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -63,10 +63,11 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
+import com.android.internal.util.DumpUtils;
import com.android.server.SystemService.TargetUser;
-import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
-import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.Objects;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executor;
@@ -122,6 +123,20 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
}
@Override
+ public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) {
+ return;
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ AppFunctionDumpHelper.dumpAppFunctionsState(mContext, pw);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
public ICancellationSignal executeAppFunction(
@NonNull ExecuteAppFunctionAidlRequest requestInternal,
@NonNull IExecuteAppFunctionCallback executeAppFunctionCallback) {
@@ -216,6 +231,9 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
"Caller does not have permission to execute the"
+ " appfunction",
/* extras= */ null));
+ throw new SecurityException(
+ "Caller does not have permission to execute the"
+ + " appfunction");
}
})
.thenCompose(
@@ -363,7 +381,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
runtimeMetadataSearchSession));
AppFunctionRuntimeMetadata newMetadata =
new AppFunctionRuntimeMetadata.Builder(existingMetadata)
- .setEnabled(enabledState).build();
+ .setEnabled(enabledState)
+ .build();
AppSearchBatchResult<String, Void> putDocumentBatchResult =
runtimeMetadataSearchSession
.put(
@@ -424,7 +443,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
targetUser,
mServiceConfig.getExecuteAppFunctionCancellationTimeoutMillis(),
cancellationSignal,
- RunAppFunctionServiceCallback.create(
+ new RunAppFunctionServiceCallback(
requestInternal,
cancellationCallback,
safeExecuteAppFunctionCallback),
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
index de2034b5be2f..b89348c038fa 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
@@ -39,20 +39,6 @@ import java.util.List;
/** A future API wrapper of {@link AppSearchSession} APIs. */
public interface FutureAppSearchSession extends Closeable {
- /** Converts a failed app search result codes into an exception. */
- @NonNull
- static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
- return switch (appSearchResult.getResultCode()) {
- case AppSearchResult.RESULT_INVALID_ARGUMENT ->
- new IllegalArgumentException(appSearchResult.getErrorMessage());
- case AppSearchResult.RESULT_IO_ERROR ->
- new IOException(appSearchResult.getErrorMessage());
- case AppSearchResult.RESULT_SECURITY_ERROR ->
- new SecurityException(appSearchResult.getErrorMessage());
- default -> new IllegalStateException(appSearchResult.getErrorMessage());
- };
- }
-
/**
* Sets the schema that represents the organizational structure of data within the AppSearch
* database.
@@ -86,17 +72,4 @@ public interface FutureAppSearchSession extends Closeable {
@Override
void close();
-
- /** A future API wrapper of {@link android.app.appsearch.SearchResults}. */
- interface FutureSearchResults {
-
- /**
- * Retrieves the next page of {@link SearchResult} objects from the {@link AppSearchSession}
- * database.
- *
- * <p>Continue calling this method to access results until it returns an empty list,
- * signifying there are no more results.
- */
- AndroidFuture<List<SearchResult>> getNextPage();
- }
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java
index d24bb871c393..87589f51cf1b 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSessionImpl.java
@@ -16,7 +16,7 @@
package com.android.server.appfunctions;
-import static com.android.server.appfunctions.FutureAppSearchSession.failedResultToException;
+import static com.android.server.appfunctions.FutureSearchResults.failedResultToException;
import android.annotation.NonNull;
import android.app.appsearch.AppSearchBatchResult;
@@ -192,33 +192,6 @@ public class FutureAppSearchSessionImpl implements FutureAppSearchSession {
});
}
- private static final class FutureSearchResultsImpl implements FutureSearchResults {
- private final SearchResults mSearchResults;
- private final Executor mExecutor;
-
- private FutureSearchResultsImpl(
- @NonNull SearchResults searchResults, @NonNull Executor executor) {
- this.mSearchResults = searchResults;
- this.mExecutor = executor;
- }
-
- @Override
- public AndroidFuture<List<SearchResult>> getNextPage() {
- AndroidFuture<AppSearchResult<List<SearchResult>>> nextPageFuture =
- new AndroidFuture<>();
-
- mSearchResults.getNextPage(mExecutor, nextPageFuture::complete);
- return nextPageFuture.thenApply(
- result -> {
- if (result.isSuccess()) {
- return result.getResultValue();
- } else {
- throw new RuntimeException(failedResultToException(result));
- }
- });
- }
- }
-
private static final class BatchResultCallbackAdapter<K, V>
implements BatchResultCallback<K, V> {
private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture;
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
index 874c5daa1662..4cc08173ac97 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.app.appsearch.AppSearchManager;
import android.app.appsearch.AppSearchResult;
import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.SearchSpec;
import android.app.appsearch.exceptions.AppSearchException;
import android.app.appsearch.observer.ObserverCallback;
import android.app.appsearch.observer.ObserverSpec;
@@ -49,12 +50,23 @@ public class FutureGlobalSearchSession implements Closeable {
return result.getResultValue();
} else {
throw new RuntimeException(
- FutureAppSearchSession.failedResultToException(result));
+ FutureSearchResults.failedResultToException(result));
}
});
}
/**
+ * Retrieves documents from the open {@link GlobalSearchSession} that match a given query string
+ * and type of search provided.
+ */
+ public AndroidFuture<FutureSearchResults> search(
+ String queryExpression, SearchSpec searchSpec) {
+ return getSessionAsync()
+ .thenApply(session -> session.search(queryExpression, searchSpec))
+ .thenApply(result -> new FutureSearchResultsImpl(result, mExecutor));
+ }
+
+ /**
* Registers an observer callback for the given target package name.
*
* @param targetPackageName The package name of the target app.
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java
new file mode 100644
index 000000000000..c38ff143178f
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResults.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+
+/** A future API wrapper of {@link android.app.appsearch.SearchResults}. */
+public interface FutureSearchResults extends Closeable {
+
+ /** Converts a failed app search result codes into an exception. */
+ @NonNull
+ public static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
+ return switch (appSearchResult.getResultCode()) {
+ case AppSearchResult.RESULT_INVALID_ARGUMENT ->
+ new IllegalArgumentException(appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_IO_ERROR ->
+ new IOException(appSearchResult.getErrorMessage());
+ case AppSearchResult.RESULT_SECURITY_ERROR ->
+ new SecurityException(appSearchResult.getErrorMessage());
+ default -> new IllegalStateException(appSearchResult.getErrorMessage());
+ };
+ }
+
+ /**
+ * Retrieves the next page of {@link SearchResult} objects from the {@link AppSearchSession}
+ * database.
+ *
+ * <p>Continue calling this method to access results until it returns an empty list, signifying
+ * there are no more results.
+ */
+ AndroidFuture<List<SearchResult>> getNextPage();
+
+ @Override
+ void close();
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java
new file mode 100644
index 000000000000..c8bc538f7226
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureSearchResultsImpl.java
@@ -0,0 +1,62 @@
+/*
+ * 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.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchResults;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class FutureSearchResultsImpl implements FutureSearchResults {
+ private final SearchResults mSearchResults;
+ private final Executor mExecutor;
+
+ public FutureSearchResultsImpl(
+ @NonNull SearchResults searchResults, @NonNull Executor executor) {
+ this.mSearchResults = searchResults;
+ this.mExecutor = executor;
+ }
+
+ @Override
+ public AndroidFuture<List<SearchResult>> getNextPage() {
+ AndroidFuture<AppSearchResult<List<SearchResult>>> nextPageFuture = new AndroidFuture<>();
+
+ mSearchResults.getNextPage(mExecutor, nextPageFuture::complete);
+ return nextPageFuture
+ .thenApply(
+ result -> {
+ if (result.isSuccess()) {
+ return result.getResultValue();
+ } else {
+ throw new RuntimeException(
+ FutureSearchResults.failedResultToException(result));
+ }
+ });
+ }
+
+ @Override
+ public void close() {
+ mSearchResults.close();
+ }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index d84b20556053..96be76975e9d 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -45,7 +45,6 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AndroidFuture;
-import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -421,26 +420,29 @@ public class MetadataSyncAdapter {
Objects.requireNonNull(propertyPackageName);
ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
- FutureSearchResults futureSearchResults =
+ try (FutureSearchResults futureSearchResults =
searchSession
.search(
"",
buildMetadataSearchSpec(
schemaType, propertyFunctionId, propertyPackageName))
- .get();
- List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
- // TODO(b/357551503): This could be expensive if we have more functions
- while (!searchResultsList.isEmpty()) {
- for (SearchResult searchResult : searchResultsList) {
- String packageName =
- searchResult.getGenericDocument().getPropertyString(propertyPackageName);
- String functionId =
- searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
- packageToFunctionIds
- .computeIfAbsent(packageName, k -> new ArraySet<>())
- .add(functionId);
+ .get(); ) {
+ List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
+ // TODO(b/357551503): This could be expensive if we have more functions
+ while (!searchResultsList.isEmpty()) {
+ for (SearchResult searchResult : searchResultsList) {
+ String packageName =
+ searchResult
+ .getGenericDocument()
+ .getPropertyString(propertyPackageName);
+ String functionId =
+ searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
+ packageToFunctionIds
+ .computeIfAbsent(packageName, k -> new ArraySet<>())
+ .add(functionId);
+ }
+ searchResultsList = futureSearchResults.getNextPage().get();
}
- searchResultsList = futureSearchResults.getNextPage().get();
}
return packageToFunctionIds;
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
index 7820390dd544..129be65f3153 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RunAppFunctionServiceCallback.java
@@ -27,17 +27,17 @@ import android.util.Slog;
import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
-
/**
* A callback to forward a request to the {@link IAppFunctionService} and report back the result.
*/
public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAppFunctionService> {
+ private static final String TAG = RunAppFunctionServiceCallback.class.getSimpleName();
private final ExecuteAppFunctionAidlRequest mRequestInternal;
private final SafeOneTimeExecuteAppFunctionCallback mSafeExecuteAppFunctionCallback;
private final ICancellationCallback mCancellationCallback;
- private RunAppFunctionServiceCallback(
+ public RunAppFunctionServiceCallback(
ExecuteAppFunctionAidlRequest requestInternal,
ICancellationCallback cancellationCallback,
SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
@@ -46,21 +46,6 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp
this.mCancellationCallback = cancellationCallback;
}
- /**
- * Creates a new instance of {@link RunAppFunctionServiceCallback}.
- *
- * @param requestInternal a request to send to the service.
- * @param cancellationCallback a callback to forward cancellation signal to the service.
- * @param safeExecuteAppFunctionCallback a callback to report back the result of the operation.
- */
- public static RunAppFunctionServiceCallback create(
- ExecuteAppFunctionAidlRequest requestInternal,
- ICancellationCallback cancellationCallback,
- SafeOneTimeExecuteAppFunctionCallback safeExecuteAppFunctionCallback) {
- return new RunAppFunctionServiceCallback(
- requestInternal, cancellationCallback, safeExecuteAppFunctionCallback);
- }
-
@Override
public void onServiceConnected(
@NonNull IAppFunctionService service,
@@ -68,6 +53,7 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp
try {
service.executeAppFunction(
mRequestInternal.getClientRequest(),
+ mRequestInternal.getCallingPackage(),
mCancellationCallback,
new IExecuteAppFunctionCallback.Stub() {
@Override
@@ -88,7 +74,7 @@ public class RunAppFunctionServiceCallback implements RunServiceCallCallback<IAp
@Override
public void onFailedToConnect() {
- Slog.e("AppFunctionManagerServiceImpl", "Failed to connect to service");
+ Slog.e(TAG, "Failed to connect to service");
mSafeExecuteAppFunctionCallback.onResult(
ExecuteAppFunctionResponse.newFailure(
ExecuteAppFunctionResponse.RESULT_APP_UNKNOWN_ERROR,
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 3bcca1c22c89..2968ff3d0df6 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -16,10 +16,13 @@
package com.android.server.companion.virtual;
+import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
+import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_ENABLED;
import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY;
import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY;
import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
@@ -425,6 +428,27 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
mDisplayManager = displayManager;
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
mPowerManager = context.getSystemService(PowerManager.class);
+
+ if (mDevicePolicies.get(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_DEFAULT)
+ != DEVICE_POLICY_DEFAULT) {
+ if (mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+ + "set a custom clipboard policy.");
+ }
+ }
+
+ int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
+ if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) {
+ if (mContext.checkCallingOrSelfPermission(ADD_ALWAYS_UNLOCKED_DISPLAY)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires ADD_ALWAYS_UNLOCKED_DISPLAY permission to "
+ + "create an always unlocked virtual device.");
+ }
+ flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
+ }
+ mBaseVirtualDisplayFlags = flags;
+
if (inputController == null) {
mInputController = new InputController(
context.getMainThreadHandler(),
@@ -467,12 +491,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
: mParams.getAllowedActivities();
}
- int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
- if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) {
- flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
- }
- mBaseVirtualDisplayFlags = flags;
-
if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) {
final String imeId = mParams.getInputMethodComponent().flattenToShortString();
Slog.d(TAG, "Setting custom input method " + imeId + " as default for virtual device "
@@ -884,8 +902,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
synchronized (mVirtualDeviceLock) {
mDevicePolicies.put(policyType, devicePolicy);
for (int i = 0; i < mVirtualDisplays.size(); i++) {
- mVirtualDisplays.valueAt(i).getWindowPolicyController()
- .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+ if (wrapper.isTrusted()) {
+ wrapper.getWindowPolicyController()
+ .setShowInHostDeviceRecents(
+ devicePolicy == DEVICE_POLICY_DEFAULT);
+ }
}
}
break;
@@ -905,7 +927,20 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
break;
case POLICY_TYPE_CLIPBOARD:
if (Flags.crossDeviceClipboard()) {
+ if (policyType == DEVICE_POLICY_CUSTOM
+ && mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+ + "set a custom clipboard policy.");
+ }
synchronized (mVirtualDeviceLock) {
+ for (int i = 0; i < mVirtualDisplays.size(); i++) {
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+ if (!wrapper.isTrusted() && !wrapper.isMirror()) {
+ throw new SecurityException("All displays must be trusted for "
+ + "devices with custom clipboard policy.");
+ }
+ }
mDevicePolicies.put(policyType, devicePolicy);
}
}
@@ -936,8 +971,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
checkDisplayOwnedByVirtualDeviceLocked(displayId);
switch (policyType) {
case POLICY_TYPE_RECENTS:
- mVirtualDisplays.get(displayId).getWindowPolicyController()
- .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId);
+ if (wrapper.isTrusted()) {
+ wrapper.getWindowPolicyController()
+ .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+ }
break;
case POLICY_TYPE_ACTIVITY:
mVirtualDisplays.get(displayId).getWindowPolicyController()
@@ -1247,10 +1285,13 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
try {
synchronized (mVirtualDeviceLock) {
mDefaultShowPointerIcon = showPointerIcon;
- }
- final int[] displayIds = getDisplayIds();
- for (int i = 0; i < displayIds.length; ++i) {
- mInputController.setShowPointerIcon(showPointerIcon, displayIds[i]);
+ for (int i = 0; i < mVirtualDisplays.size(); i++) {
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+ if (wrapper.isTrusted() || wrapper.isMirror()) {
+ mInputController.setShowPointerIcon(
+ mDefaultShowPointerIcon, mVirtualDisplays.keyAt(i));
+ }
+ }
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -1491,6 +1532,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
boolean isTrustedDisplay =
(mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
== Display.FLAG_TRUSTED;
+ if (!isTrustedDisplay) {
+ if (getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
+ throw new SecurityException("All displays must be trusted for devices with custom"
+ + "clipboard policy.");
+ }
+ }
boolean showPointer;
synchronized (mVirtualDeviceLock) {
@@ -1500,7 +1547,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
"Virtual device already has a virtual display with ID " + displayId);
}
- PowerManager.WakeLock wakeLock = createAndAcquireWakeLockForDisplay(displayId);
+ PowerManager.WakeLock wakeLock =
+ isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null;
mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
isTrustedDisplay, isMirrorDisplay));
showPointer = mDefaultShowPointerIcon;
@@ -1508,14 +1556,15 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
final long token = Binder.clearCallingIdentity();
try {
- mInputController.setShowPointerIcon(showPointer, displayId);
mInputController.setMousePointerAccelerationEnabled(false, displayId);
mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
displayId);
- // WM throws a SecurityException if the display is untrusted.
if (isTrustedDisplay) {
+ mInputController.setShowPointerIcon(showPointer, displayId);
mInputController.setDisplayImePolicy(displayId,
WindowManager.DISPLAY_IME_POLICY_LOCAL);
+ } else {
+ gwpc.setShowInHostDeviceRecents(true);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -1616,6 +1665,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
!= PackageManager.PERMISSION_GRANTED) {
synchronized (mVirtualDeviceLock) {
checkDisplayOwnedByVirtualDeviceLocked(displayId);
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId);
+ if (!wrapper.isTrusted() && !wrapper.isMirror()) {
+ throw new SecurityException(
+ "Cannot create input device associated with an untrusted display");
+ }
}
}
}
@@ -1665,7 +1719,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
* @param virtualDisplayWrapper - VirtualDisplayWrapper to release resources for.
*/
private void releaseOwnedVirtualDisplayResources(VirtualDisplayWrapper virtualDisplayWrapper) {
- virtualDisplayWrapper.getWakeLock().release();
+ virtualDisplayWrapper.releaseWakeLock();
virtualDisplayWrapper.getWindowPolicyController().unregisterRunningAppsChangedListener(
this);
}
@@ -1833,10 +1887,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
VirtualDisplayWrapper(@NonNull IVirtualDisplayCallback token,
@NonNull GenericWindowPolicyController windowPolicyController,
- @NonNull PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) {
+ @Nullable PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) {
mToken = Objects.requireNonNull(token);
mWindowPolicyController = Objects.requireNonNull(windowPolicyController);
- mWakeLock = Objects.requireNonNull(wakeLock);
+ mWakeLock = wakeLock;
mIsTrusted = isTrusted;
mIsMirror = isMirror;
}
@@ -1845,8 +1899,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
return mWindowPolicyController;
}
- PowerManager.WakeLock getWakeLock() {
- return mWakeLock;
+ void releaseWakeLock() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
}
boolean isTrusted() {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index e8f7b5f00152..776a3455acc4 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2553,7 +2553,7 @@ public class OomAdjuster {
double cachedRestoreThreshold =
mProcessList.getCachedRestoreThresholdKb() * thresholdModifier;
- if (isLastMemoryLevelNormal() && lastPssOrRss >= cachedRestoreThreshold) {
+ if (!isLastMemoryLevelNormal() && lastPssOrRss >= cachedRestoreThreshold) {
state.setServiceHighRam(true);
state.setServiceB(true);
//Slog.i(TAG, "ADJ " + app + " high ram!");
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 2485626a8f42..5236b0399f25 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3629,42 +3629,68 @@ public final class ProcessList {
}
@GuardedBy({"mService", "mProcLock"})
- private int updateLruProcessInternalLSP(ProcessRecord app, long now, int index,
- int lruSeq, String what, Object obj, ProcessRecord srcApp) {
+ private int offerLruProcessInternalLSP(ProcessRecord app, long now, String what, Object obj,
+ ProcessRecord srcApp) {
app.setLastActivityTime(now);
if (app.hasActivitiesOrRecentTasks()) {
// Don't want to touch dependent processes that are hosting activities.
- return index;
+ return -1;
}
- int lrui = mLruProcesses.lastIndexOf(app);
+ final int lrui = mLruProcesses.lastIndexOf(app);
if (lrui < 0) {
Slog.wtf(TAG, "Adding dependent process " + app + " not on LRU list: "
+ what + " " + obj + " from " + srcApp);
- return index;
}
+ return lrui;
+ }
- if (lrui >= index) {
- // Don't want to cause this to move dependent processes *back* in the
- // list as if they were less frequently used.
- return index;
- }
+ /**
+ * This method is called after the indices array is populated by the indices offered by
+ * {@link #offerLruProcessInternalLSP} to actually move the processes to the desired locations
+ * in the LRU list. Since the indices array is a SparseBooleanArray, the indices are sorted
+ * and this allows us to preserve the previous order of the processes relative to each other.
+ * Key of the indices array holds the current index of the process in the LRU list and the value
+ * is a boolean indicating whether the process is an activity process or not. Activity processes
+ * are moved to the nextActivityIndex and non-activity processes are moved to the nextIndex
+ * positions, which are provided by the caller.
+ *
+ * @param indices The indices of the processes to move.
+ * @param nextActivityIndex The next index to insert an activity process.
+ * @param nextIndex The next index to insert a non-activity process.
+ */
+ @GuardedBy({"mService", "mProcLock"})
+ private void completeLruProcessInternalLSP(SparseBooleanArray indices, int nextActivityIndex,
+ int nextIndex) {
+ for (int i = indices.size() - 1; i >= 0; i--) {
+ final int lrui = indices.keyAt(i);
+ if (lrui < 0) {
+ // Rest of the indices are invalid, we can return early.
+ return;
+ }
+ final boolean isActivity = indices.valueAt(i);
+ int index = isActivity ? nextActivityIndex : nextIndex;
- if (lrui >= mLruProcessActivityStart && index < mLruProcessActivityStart) {
- // Don't want to touch dependent processes that are hosting activities.
- return index;
- }
+ if (lrui >= index) {
+ // Don't want to cause this to move dependent processes *back* in the
+ // list as if they were less frequently used.
+ continue;
+ }
- mLruProcesses.remove(lrui);
- if (index > 0) {
+ final ProcessRecord app = mLruProcesses.remove(lrui);
index--;
+ if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index
+ + " in LRU list: " + app);
+ mLruProcesses.add(index, app);
+ app.setLruSeq(mLruSeq);
+
+ if (isActivity) {
+ nextActivityIndex = index;
+ } else {
+ nextIndex = index;
+ }
}
- if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index
- + " in LRU list: " + app);
- mLruProcesses.add(index, app);
- app.setLruSeq(lruSeq);
- return index;
}
/**
@@ -4058,6 +4084,15 @@ public final class ProcessList {
app.setLruSeq(mLruSeq);
+ // Key of the indices array holds the current index of the process in the LRU list and the
+ // value is a boolean indicating whether the process is an activity process or not.
+ // Activity processes will be moved to the nextActivityIndex and non-activity processes will
+ // be moved to the nextIndex positions when completeLruProcessInternalLSP is called.
+ // Since SparseBooleanArray's keys are sorted, we'll be able to keep the existing order of
+ // the processes relative to each other after the move.
+ final SparseBooleanArray indices = new SparseBooleanArray(psr.numberOfConnections()
+ + app.mProviders.numberOfProviderConnections());
+
// If the app is currently using a content provider or service,
// bump those processes as well.
for (int j = psr.numberOfConnections() - 1; j >= 0; j--) {
@@ -4069,16 +4104,12 @@ public final class ProcessList {
&& !cr.binding.service.app.isPersistent()) {
if (cr.binding.service.app.mServices.hasClientActivities()) {
if (nextActivityIndex >= 0) {
- nextActivityIndex = updateLruProcessInternalLSP(cr.binding.service.app,
- now,
- nextActivityIndex, mLruSeq,
- "service connection", cr, app);
+ indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now,
+ "service connection", cr, app), true);
}
} else {
- nextIndex = updateLruProcessInternalLSP(cr.binding.service.app,
- now,
- nextIndex, mLruSeq,
- "service connection", cr, app);
+ indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now,
+ "service connection", cr, app), false);
}
}
}
@@ -4086,10 +4117,11 @@ public final class ProcessList {
for (int j = ppr.numberOfProviderConnections() - 1; j >= 0; j--) {
ContentProviderRecord cpr = ppr.getProviderConnectionAt(j).provider;
if (cpr.proc != null && cpr.proc.getLruSeq() != mLruSeq && !cpr.proc.isPersistent()) {
- nextIndex = updateLruProcessInternalLSP(cpr.proc, now, nextIndex, mLruSeq,
- "provider reference", cpr, app);
+ indices.append(offerLruProcessInternalLSP(cpr.proc, now,
+ "provider reference", cpr, app), false);
}
}
+ completeLruProcessInternalLSP(indices, nextActivityIndex, nextIndex);
}
@GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index e0cf96fbccd0..e97629bfd3e1 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -72,6 +72,10 @@ import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION;
import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
import android.Manifest;
@@ -160,6 +164,7 @@ import com.android.internal.os.Clock;
import com.android.internal.pm.pkg.component.ParsedAttribution;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -2829,12 +2834,26 @@ public class AppOpsService extends IAppOpsService.Stub {
@Override
public int checkOperation(int code, int uid, String packageName) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+ false);
+ }
return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
Context.DEVICE_ID_DEFAULT, false /*raw*/);
}
@Override
public int checkOperationForDevice(int code, int uid, String packageName, int virtualDeviceId) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
+ false);
+ }
return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
virtualDeviceId, false /*raw*/);
}
@@ -3015,6 +3034,13 @@ public class AppOpsService extends IAppOpsService.Stub {
public SyncNotedAppOp noteProxyOperationWithState(int code,
AttributionSourceState attributionSourceState, boolean shouldCollectAsyncNotedOp,
String message, boolean shouldCollectMessage, boolean skipProxyOperation) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, attributionSourceState.uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION,
+ attributionSourceState.attributionTag != null);
+ }
AttributionSource attributionSource = new AttributionSource(attributionSourceState);
return mCheckOpsDelegateDispatcher.noteProxyOperation(code, attributionSource,
shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation);
@@ -3096,6 +3122,13 @@ public class AppOpsService extends IAppOpsService.Stub {
public SyncNotedAppOp noteOperation(int code, int uid, String packageName,
String attributionTag, boolean shouldCollectAsyncNotedOp, String message,
boolean shouldCollectMessage) {
+ if (Binder.getCallingPid() != Process.myPid()
+ && Flags.appopAccessTrackingLoggingEnabled()) {
+ FrameworkStatsLog.write(
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
+ APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
+ attributionTag != null);
+ }
return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName,
attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message,
shouldCollectMessage);
diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
index 03c81560be89..ed41f2e881f8 100644
--- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
+++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java
@@ -36,6 +36,7 @@ import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
import static android.app.AppOpsManager.UID_STATE_NONEXISTENT;
import static android.app.AppOpsManager.UID_STATE_TOP;
+import static android.permission.flags.Flags.delayUidStateChangesFromCapabilityUpdates;
import static android.permission.flags.Flags.finishRunningOpsForKilledPackages;
import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState;
@@ -236,20 +237,26 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker {
mPendingUidStates.put(uid, uidState);
mPendingCapability.put(uid, capability);
+ boolean hasLostCapability = (prevCapability & ~capability) != 0;
+
if (procState == PROCESS_STATE_NONEXISTENT) {
mPendingGone.put(uid, true);
commitUidPendingState(uid);
- } else if (uidState < prevUidState
- || (uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
- && prevUidState > UID_STATE_MAX_LAST_NON_RESTRICTED)) {
+ } else if (uidState < prevUidState) {
// We are moving to a more important state, or the new state may be in the
// foreground and the old state is in the background, then always do it
// immediately.
commitUidPendingState(uid);
- } else if (uidState == prevUidState && capability != prevCapability) {
+ } else if (delayUidStateChangesFromCapabilityUpdates()
+ && uidState == prevUidState && !hasLostCapability) {
+ // No change on process state, but process capability hasn't decreased.
+ commitUidPendingState(uid);
+ } else if (!delayUidStateChangesFromCapabilityUpdates()
+ && uidState == prevUidState && capability != prevCapability) {
// No change on process state, but process capability has changed.
commitUidPendingState(uid);
- } else if (uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED) {
+ } else if (uidState <= UID_STATE_MAX_LAST_NON_RESTRICTED
+ && (!delayUidStateChangesFromCapabilityUpdates() || !hasLostCapability)) {
// We are moving to a less important state, but it doesn't cross the restriction
// threshold.
commitUidPendingState(uid);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1563a6257552..fdf7dec31cad 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -286,7 +286,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
-import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -2748,6 +2747,11 @@ public class AudioService extends IAudioService.Stub
}
}
+ @Override
+ protected void onUnhandledException(int code, int flags, Exception e) {
+ Slog.wtf(TAG, "Uncaught exception in AudioService: " + code + ", " + flags, e);
+ }
+
@Override // Binder call
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
@@ -8044,7 +8048,14 @@ public class AudioService extends IAudioService.Stub
}
synchronized (mAbsoluteVolumeDeviceInfoMapLock) {
if (mAbsoluteVolumeDeviceInfoMap.containsKey(audioSystemDeviceOut)) {
- return mAbsoluteVolumeDeviceInfoMap.get(audioSystemDeviceOut).mDeviceVolumeBehavior;
+ final AbsoluteVolumeDeviceInfo deviceInfo = mAbsoluteVolumeDeviceInfoMap.get(
+ audioSystemDeviceOut);
+ if (deviceInfo != null) {
+ return deviceInfo.mDeviceVolumeBehavior;
+ }
+
+ Log.e(TAG,
+ "Null absolute volume device info stored for key " + audioSystemDeviceOut);
}
}
@@ -15038,6 +15049,11 @@ public class AudioService extends IAudioService.Stub
private void addAudioSystemDeviceOutToAbsVolumeDevices(int audioSystemDeviceOut,
AbsoluteVolumeDeviceInfo info) {
+ if (info == null) {
+ Log.e(TAG, "Cannot add null absolute volume info for audioSystemDeviceOut "
+ + audioSystemDeviceOut);
+ return;
+ }
if (DEBUG_VOL) {
Log.d(TAG, "Adding DeviceType: 0x" + Integer.toHexString(audioSystemDeviceOut)
+ " to mAbsoluteVolumeDeviceInfoMap with behavior "
diff --git a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
index 01f770b1e89f..9fa5da47aae7 100644
--- a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
+++ b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
@@ -133,6 +133,9 @@ public class LoudnessCodecHelper {
private static final EventLogger sLogger = new EventLogger(
AudioService.LOG_NB_EVENTS_LOUDNESS_CODEC, "Loudness updates");
+ private final Object mDispatcherLock = new Object();
+
+ @GuardedBy("mDispatcherLock")
private final LoudnessRemoteCallbackList mLoudnessUpdateDispatchers =
new LoudnessRemoteCallbackList(this);
@@ -339,12 +342,16 @@ public class LoudnessCodecHelper {
}
void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
- mLoudnessUpdateDispatchers.register(dispatcher, Binder.getCallingPid());
+ synchronized (mDispatcherLock) {
+ mLoudnessUpdateDispatchers.register(dispatcher, Binder.getCallingPid());
+ }
}
void unregisterLoudnessCodecUpdatesDispatcher(
ILoudnessCodecUpdatesDispatcher dispatcher) {
- mLoudnessUpdateDispatchers.unregister(dispatcher);
+ synchronized (mDispatcherLock) {
+ mLoudnessUpdateDispatchers.unregister(dispatcher);
+ }
}
void startLoudnessCodecUpdates(int sessionId) {
@@ -640,17 +647,20 @@ public class LoudnessCodecHelper {
Log.d(TAG,
"dispatchNewLoudnessParameters: sessionId " + sessionId + " bundle: " + bundle);
}
- final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast();
- for (int i = 0; i < nbDispatchers; ++i) {
- try {
- mLoudnessUpdateDispatchers.getBroadcastItem(i)
- .dispatchLoudnessCodecParameterChange(sessionId, bundle);
- } catch (RemoteException e) {
- Log.e(TAG, "Error dispatching for sessionId " + sessionId + " bundle: " + bundle,
- e);
+ synchronized (mDispatcherLock) {
+ final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast();
+ for (int i = 0; i < nbDispatchers; ++i) {
+ try {
+ mLoudnessUpdateDispatchers.getBroadcastItem(i)
+ .dispatchLoudnessCodecParameterChange(sessionId, bundle);
+ } catch (RemoteException e) {
+ Log.e(TAG,
+ "Error dispatching for sessionId " + sessionId + " bundle: " + bundle,
+ e);
+ }
}
+ mLoudnessUpdateDispatchers.finishBroadcast();
}
- mLoudnessUpdateDispatchers.finishBroadcast();
}
@GuardedBy("mLock")
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index a9fe8cb01b3a..8d64383b32b9 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -242,7 +242,8 @@ public class PlatformCompat extends IPlatformCompat.Stub {
boolean enabled = true;
final int userId = UserHandle.getUserId(uid);
for (String packageName : packages) {
- final var appInfo = getApplicationInfo(packageName, userId);
+ final var appInfo =
+ fixTargetSdk(getApplicationInfo(packageName, userId), uid);
enabled &= isChangeEnabledInternal(changeId, appInfo);
}
return enabled;
@@ -261,7 +262,8 @@ public class PlatformCompat extends IPlatformCompat.Stub {
boolean enabled = true;
final int userId = UserHandle.getUserId(uid);
for (String packageName : packages) {
- final var appInfo = getApplicationInfo(packageName, userId);
+ final var appInfo =
+ fixTargetSdk(getApplicationInfo(packageName, userId), uid);
enabled &= isChangeEnabledInternalNoLogging(changeId, appInfo);
}
return enabled;
@@ -504,6 +506,15 @@ public class PlatformCompat extends IPlatformCompat.Stub {
packageName, 0, Process.myUid(), userId);
}
+ private ApplicationInfo fixTargetSdk(ApplicationInfo appInfo, int uid) {
+ // b/282922910 - we don't want apps sharing system uid and targeting
+ // older target sdk to impact all system uid apps
+ if (Flags.systemUidTargetSystemSdk() && uid == Process.SYSTEM_UID) {
+ appInfo.targetSdkVersion = Build.VERSION.SDK_INT;
+ }
+ return appInfo;
+ }
+
private void killPackage(String packageName) {
int uid = LocalServices.getService(PackageManagerInternal.class).getPackageUid(packageName,
0, UserHandle.myUserId());
diff --git a/services/core/java/com/android/server/compat/platform_compat_flags.aconfig b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig
new file mode 100644
index 000000000000..fb323238c38e
--- /dev/null
+++ b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.compat"
+container: "system"
+
+flag {
+ name: "system_uid_target_system_sdk"
+ namespace: "app_compat"
+ description: "Compat framework feature flag for forcing all system uid apps to target system sdk"
+ bug: "29702703"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
index 3eb3380a57a0..2e2a93776f9d 100644
--- a/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
+++ b/services/core/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
@@ -18,7 +18,7 @@ package com.android.server.crashrecovery;
import android.os.Environment;
import android.util.IndentingPrintWriter;
-import android.util.Slog;
+import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
@@ -41,7 +41,7 @@ public class CrashRecoveryUtils {
/** Persist recovery related events in crashrecovery events file.**/
public static void logCrashRecoveryEvent(int priority, String msg) {
- Slog.println(priority, TAG, msg);
+ Log.println(priority, TAG, msg);
try {
File fname = getCrashRecoveryEventsFile();
synchronized (sFileLock) {
@@ -52,7 +52,7 @@ public class CrashRecoveryUtils {
pw.close();
}
} catch (IOException e) {
- Slog.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage());
+ Log.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage());
}
}
@@ -72,7 +72,7 @@ public class CrashRecoveryUtils {
pw.println(line);
}
} catch (IOException e) {
- Slog.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage());
+ Log.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage());
}
}
pw.decreaseIndent();
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 8d96ba93530b..c4e10360a7cb 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -150,7 +150,7 @@ import javax.xml.datatype.DatatypeConfigurationException;
* <screenBrightnessDefault>0.65</screenBrightnessDefault>
* <powerThrottlingConfig>
* <brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed>
- * <customAnimationRateSec>0.004</customAnimationRateSec>
+ * <customAnimationRate>0.004</customAnimationRate>
* <pollingWindowMaxMillis>30000</pollingWindowMaxMillis>
* <pollingWindowMinMillis>10000</pollingWindowMinMillis>
* <powerThrottlingMap>
@@ -2193,11 +2193,11 @@ public class DisplayDeviceConfig {
return;
}
float lowestBrightnessCap = powerThrottlingCfg.getBrightnessLowestCapAllowed().floatValue();
- float customAnimationRateSec = powerThrottlingCfg.getCustomAnimationRateSec().floatValue();
+ float customAnimationRate = powerThrottlingCfg.getCustomAnimationRate().floatValue();
int pollingWindowMaxMillis = powerThrottlingCfg.getPollingWindowMaxMillis().intValue();
int pollingWindowMinMillis = powerThrottlingCfg.getPollingWindowMinMillis().intValue();
mPowerThrottlingConfigData = new PowerThrottlingConfigData(lowestBrightnessCap,
- customAnimationRateSec,
+ customAnimationRate,
pollingWindowMaxMillis,
pollingWindowMinMillis);
}
@@ -3012,16 +3012,16 @@ public class DisplayDeviceConfig {
/** Lowest brightness cap allowed for this device. */
public final float brightnessLowestCapAllowed;
/** Time take to animate brightness in seconds. */
- public final float customAnimationRateSec;
+ public final float customAnimationRate;
/** Time window for maximum polling power in milliseconds. */
public final int pollingWindowMaxMillis;
/** Time window for minimum polling power in milliseconds. */
public final int pollingWindowMinMillis;
public PowerThrottlingConfigData(float brightnessLowestCapAllowed,
- float customAnimationRateSec, int pollingWindowMaxMillis,
+ float customAnimationRate, int pollingWindowMaxMillis,
int pollingWindowMinMillis) {
this.brightnessLowestCapAllowed = brightnessLowestCapAllowed;
- this.customAnimationRateSec = customAnimationRateSec;
+ this.customAnimationRate = customAnimationRate;
this.pollingWindowMaxMillis = pollingWindowMaxMillis;
this.pollingWindowMinMillis = pollingWindowMinMillis;
}
@@ -3031,7 +3031,7 @@ public class DisplayDeviceConfig {
return "PowerThrottlingConfigData{"
+ "brightnessLowestCapAllowed: "
+ brightnessLowestCapAllowed
- + ", customAnimationRateSec: " + customAnimationRateSec
+ + ", customAnimationRate: " + customAnimationRate
+ ", pollingWindowMaxMillis: " + pollingWindowMaxMillis
+ ", pollingWindowMinMillis: " + pollingWindowMinMillis
+ "} ";
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 179ec63458dd..c7a70fafbf26 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1769,10 +1769,11 @@ public final class DisplayManagerService extends SystemService {
flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
}
// Put the display in the virtual device's display group only if it's not a mirror display,
- // and if it doesn't need its own display group. So effectively, mirror displays go into the
- // default display group.
+ // it is a trusted display, and it doesn't need its own display group. So effectively,
+ // mirror and untrusted displays go into the default display group.
if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0
&& (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
+ && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == VIRTUAL_DISPLAY_FLAG_TRUSTED
&& virtualDevice != null) {
flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
}
@@ -1848,9 +1849,7 @@ public final class DisplayManagerService extends SystemService {
if (callingUid != Process.SYSTEM_UID
&& (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
- // The virtualDevice instance has been validated above using isValidVirtualDevice
- if (virtualDevice == null
- && !checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) {
+ if (!checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) {
throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+ "create a virtual display which is not in the default DisplayGroup.");
}
@@ -5667,6 +5666,11 @@ public final class DisplayManagerService extends SystemService {
displayPowerController.stylusGestureStarted(eventTime);
}
}
+
+ @Override
+ public boolean isDisplayReadyForMirroring(int displayId) {
+ return mExternalDisplayPolicy.isDisplayReadyForMirroring(displayId);
+ }
}
class DesiredDisplayModeSpecsObserver
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index 28a0b28a0167..f34d2cc6e684 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -375,6 +375,54 @@ class ExternalDisplayPolicy {
}
}
+ boolean isDisplayReadyForMirroring(int displayId) {
+ if (!mFlags.isWaitingConfirmationBeforeMirroringEnabled()) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring CONFIRMED - "
+ + " flag 'waiting for confirmation before mirroring' is disabled");
+ }
+ return true;
+ }
+
+ synchronized (mSyncRoot) {
+ if (!mIsBootCompleted) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "boot is in progress");
+ }
+ return false;
+ }
+
+ var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
+ if (logicalDisplay == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "logicalDisplay is null");
+ }
+ return false;
+ }
+
+ if (!isExternalDisplayLocked(logicalDisplay)) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "logicalDisplay" + logicalDisplay.getDisplayIdLocked()
+ + " type is " + logicalDisplay.getDisplayInfoLocked().type);
+ }
+ return false;
+ }
+
+ if (!logicalDisplay.isEnabledLocked()) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "logicalDisplay is disabled");
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
@Override
public void notifyThrottling(@NonNull final Temperature temp) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 06a910396d6c..09fa4e6aa628 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY;
import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP;
import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
@@ -594,6 +595,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener {
boolean shouldDeviceBeWoken(DeviceState pendingState, DeviceState currentState,
boolean isInteractive, boolean isBootCompleted) {
if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) {
+ if (currentState.hasProperties(PROPERTY_EMULATED_ONLY)
+ && !pendingState.hasProperties(PROPERTY_EMULATED_ONLY)) {
+ // Do not wake the device, since this transition may occur due to the user pressing
+ // the power button to exit an emulated state.
+ return false;
+ }
+
return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
&& !currentState.equals(INVALID_DEVICE_STATE)
&& !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java
index 85e81f989845..1a18b004a3e2 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessPowerClamper.java
@@ -85,7 +85,7 @@ class BrightnessPowerClamper extends
private String mDataId = null;
private float mCurrentBrightness = PowerManager.BRIGHTNESS_INVALID;
private float mCustomAnimationRateSec = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
- private float mCustomAnimationRateSecDeviceConfig =
+ private float mCustomAnimationRateDeviceConfig =
DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
private final BiFunction<String, String, ThrottlingLevel> mDataPointMapper = (key, value) -> {
try {
@@ -117,7 +117,7 @@ class BrightnessPowerClamper extends
};
mPowerThrottlingConfigData = powerData.getPowerThrottlingConfigData();
if (mPowerThrottlingConfigData != null) {
- mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec;
+ mCustomAnimationRateDeviceConfig = mPowerThrottlingConfigData.customAnimationRate;
}
mThermalLevelListener = new ThermalLevelListener(handler);
mPmicMonitor =
@@ -228,10 +228,6 @@ class BrightnessPowerClamper extends
}
mPowerThrottlingConfigData = data.getPowerThrottlingConfigData();
- if (mPowerThrottlingConfigData == null) {
- Slog.d(TAG,
- "Power throttling data is missing for configuration data.");
- }
}
private void recalculateBrightnessCap() {
@@ -282,13 +278,13 @@ class BrightnessPowerClamper extends
mIsActive = isActive;
Slog.i(TAG, "Power clamper changing current brightness cap mBrightnessCap: "
+ mBrightnessCap + " to target brightness cap:" + targetBrightnessCap
- + " for current screen brightness: " + mCurrentBrightness);
- mBrightnessCap = targetBrightnessCap;
- Slog.i(TAG, "Power clamper changed state: thermalStatus:" + mCurrentThermalLevel
+ + " for current screen brightness: " + mCurrentBrightness + "\n"
+ + "Power clamper changed state: thermalStatus:" + mCurrentThermalLevel
+ " mCurrentThermalLevelChanged:" + mCurrentThermalLevelChanged
+ " mCurrentAvgPowerConsumed:" + mCurrentAvgPowerConsumed
- + " mCustomAnimationRateSec:" + mCustomAnimationRateSecDeviceConfig);
- mCustomAnimationRateSec = mCustomAnimationRateSecDeviceConfig;
+ + " mCustomAnimationRateSec:" + mCustomAnimationRateDeviceConfig);
+ mBrightnessCap = targetBrightnessCap;
+ mCustomAnimationRateSec = mCustomAnimationRateDeviceConfig;
mChangeListener.onChanged();
} else {
mCustomAnimationRateSec = DisplayBrightnessState.CUSTOM_ANIMATION_RATE_NOT_SET;
@@ -344,7 +340,7 @@ class BrightnessPowerClamper extends
+ mPowerThrottlingConfigData.pollingWindowMinMillis + " msec.");
return;
}
- mCustomAnimationRateSecDeviceConfig = mPowerThrottlingConfigData.customAnimationRateSec;
+ mCustomAnimationRateDeviceConfig = mPowerThrottlingConfigData.customAnimationRate;
mThermalLevelListener.start();
}
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 99ced7f8bb00..b2e98bc05e75 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -217,6 +217,11 @@ public class DisplayManagerFlags {
Flags::enableUserRefreshRateForExternalDisplay
);
+ private final FlagState mEnableWaitingConfirmationBeforeMirroring = new FlagState(
+ Flags.FLAG_ENABLE_WAITING_CONFIRMATION_BEFORE_MIRRORING,
+ Flags::enableWaitingConfirmationBeforeMirroring
+ );
+
private final FlagState mEnableBatteryStatsForAllDisplays = new FlagState(
Flags.FLAG_ENABLE_BATTERY_STATS_FOR_ALL_DISPLAYS,
Flags::enableBatteryStatsForAllDisplays
@@ -445,6 +450,14 @@ public class DisplayManagerFlags {
}
/**
+ * @return {@code true} if mirroring won't be enabled until boot completes and the user enables
+ * the display.
+ */
+ public boolean isWaitingConfirmationBeforeMirroringEnabled() {
+ return mEnableWaitingConfirmationBeforeMirroring.isEnabled();
+ }
+
+ /**
* @return {@code true} if battery stats is enabled for all displays, not just the primary
* display.
*/
@@ -511,6 +524,7 @@ public class DisplayManagerFlags {
pw.println(" " + mVirtualDisplayLimit);
pw.println(" " + mNormalBrightnessForDozeParameter);
pw.println(" " + mIdleScreenConfigInSubscribingLightSensor);
+ pw.println(" " + mEnableWaitingConfirmationBeforeMirroring);
pw.println(" " + mEnableBatteryStatsForAllDisplays);
pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage);
pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 2f04d9e5fdbb..df626385c5cc 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -367,6 +367,17 @@ flag {
}
flag {
+ name: "enable_waiting_confirmation_before_mirroring"
+ namespace: "display_manager"
+ description: "Allow ContentRecorder checking whether user confirmed mirroring after boot"
+ bug: "361698995"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_battery_stats_for_all_displays"
namespace: "display_manager"
description: "Flag to enable battery stats for all displays."
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index d70bd8b17ddf..d1a6d3b9bb00 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -63,6 +63,12 @@ class InputSettingsObserver extends ContentObserver {
mObservers = Map.ofEntries(
Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED),
(reason) -> updateMousePointerSpeed()),
+ Map.entry(Settings.System.getUriFor(
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING),
+ (reason) -> updateMouseReverseVerticalScrolling()),
+ Map.entry(Settings.System.getUriFor(
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON),
+ (reason) -> updateMouseSwapPrimaryButton()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED),
(reason) -> updateTouchpadPointerSpeed()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING),
@@ -163,6 +169,16 @@ class InputSettingsObserver extends ContentObserver {
mNative.setPointerSpeed(constrainPointerSpeedValue(speed));
}
+ private void updateMouseReverseVerticalScrolling() {
+ mNative.setMouseReverseVerticalScrollingEnabled(
+ InputSettings.isMouseReverseVerticalScrollingEnabled(mContext));
+ }
+
+ private void updateMouseSwapPrimaryButton() {
+ mNative.setMouseSwapPrimaryButtonEnabled(
+ InputSettings.isMouseSwapPrimaryButtonEnabled(mContext));
+ }
+
private void updateTouchpadPointerSpeed() {
mNative.setTouchpadPointerSpeed(
constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext)));
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 4404d63e02fc..21e8bccd2883 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -127,6 +127,10 @@ interface NativeInputManagerService {
void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
+ void setMouseReverseVerticalScrollingEnabled(boolean enabled);
+
+ void setMouseSwapPrimaryButtonEnabled(boolean enabled);
+
void setTouchpadPointerSpeed(int speed);
void setTouchpadNaturalScrollingEnabled(boolean enabled);
@@ -388,6 +392,12 @@ interface NativeInputManagerService {
public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
@Override
+ public native void setMouseReverseVerticalScrollingEnabled(boolean enabled);
+
+ @Override
+ public native void setMouseSwapPrimaryButtonEnabled(boolean enabled);
+
+ @Override
public native void setTouchpadPointerSpeed(int speed);
@Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f0fb33eaaee7..35b517118aab 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -84,6 +84,7 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
+import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputManager;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.InputMethodService.BackDispositionMode;
@@ -119,6 +120,7 @@ import android.util.Printer;
import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
+import android.view.Display;
import android.view.InputChannel;
import android.view.InputDevice;
import android.view.MotionEvent;
@@ -448,6 +450,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
private AudioManagerInternal mAudioManagerInternal = null;
@Nullable
private VirtualDeviceManagerInternal mVdmInternal = null;
+ @Nullable
+ private DisplayManagerInternal mDisplayManagerInternal = null;
// Mapping from deviceId to the device-specific imeId for that device.
@GuardedBy("ImfLock.class")
@@ -2165,7 +2169,18 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl.
final var bindingController = getInputMethodBindingController(userId);
final int oldDeviceId = bindingController.getDeviceIdToShowIme();
final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
- final int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme);
+ int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme);
+ if (newDeviceId != DEVICE_ID_DEFAULT) {
+ // Only show custom IME on trusted displays.
+ if (mDisplayManagerInternal == null) {
+ mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+ }
+ int displayFlags = mDisplayManagerInternal.getDisplayInfo(displayIdToShowIme).flags;
+ if ((displayFlags & Display.FLAG_TRUSTED) != Display.FLAG_TRUSTED) {
+ // If the display is not trusted, fallback to the default device IME.
+ newDeviceId = DEVICE_ID_DEFAULT;
+ }
+ }
bindingController.setDeviceIdToShowIme(newDeviceId);
if (newDeviceId == DEVICE_ID_DEFAULT) {
if (oldDeviceId == DEVICE_ID_DEFAULT) {
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 3f4a9bb4d864..ed69f7ad32f6 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -444,8 +444,17 @@ public class ContextHubService extends IContextHubService.Stub {
mSupportedContextHubPerms = hubInfo.second;
mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values());
mClientManager = new ContextHubClientManager(mContext, mContextHubWrapper);
- mTransactionManager = new ContextHubTransactionManager(
- mContextHubWrapper, mClientManager, mNanoAppStateManager);
+
+ if (Flags.reduceLockingContextHubTransactionManager()) {
+ mTransactionManager =
+ new ContextHubTransactionManager(
+ mContextHubWrapper, mClientManager, mNanoAppStateManager);
+ } else {
+ mTransactionManager =
+ new ContextHubTransactionManagerOld(
+ mContextHubWrapper, mClientManager, mNanoAppStateManager);
+ }
+
mSensorPrivacyManagerInternal =
LocalServices.getService(SensorPrivacyManagerInternal.class);
return true;
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index 2a0b1afde27b..da31bf29a8e8 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -26,6 +26,8 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Collections;
@@ -43,8 +45,8 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Manages transactions at the Context Hub Service.
- * <p>
- * This class maintains a queue of transaction requests made to the ContextHubService by clients,
+ *
+ * <p>This class maintains a queue of transaction requests made to the ContextHubService by clients,
* and executes them through the Context Hub. At any point in time, either the transaction queue is
* empty, or there is a pending transaction that is waiting for an asynchronous response from the
* hub. This class also handles synchronous errors and timeouts of each transaction.
@@ -52,66 +54,80 @@ import java.util.concurrent.atomic.AtomicInteger;
* @hide
*/
/* package */ class ContextHubTransactionManager {
- private static final String TAG = "ContextHubTransactionManager";
+ protected static final String TAG = "ContextHubTransactionManager";
public static final Duration RELIABLE_MESSAGE_TIMEOUT = Duration.ofSeconds(1);
public static final Duration RELIABLE_MESSAGE_DUPLICATE_DETECTION_TIMEOUT =
RELIABLE_MESSAGE_TIMEOUT.multipliedBy(3);
- private static final int MAX_PENDING_REQUESTS = 10000;
+ // TODO: b/362299144: When cleaning up the flag
+ // reduce_locking_context_hub_transaction_manager, change these to private
+ protected static final int MAX_PENDING_REQUESTS = 10000;
- private static final int RELIABLE_MESSAGE_MAX_NUM_RETRY = 3;
+ protected static final int RELIABLE_MESSAGE_MAX_NUM_RETRY = 3;
- private static final Duration RELIABLE_MESSAGE_RETRY_WAIT_TIME = Duration.ofMillis(250);
+ protected static final Duration RELIABLE_MESSAGE_RETRY_WAIT_TIME = Duration.ofMillis(250);
- private static final Duration RELIABLE_MESSAGE_MIN_WAIT_TIME = Duration.ofNanos(1000);
+ protected static final Duration RELIABLE_MESSAGE_MIN_WAIT_TIME = Duration.ofNanos(1000);
- private final IContextHubWrapper mContextHubProxy;
+ protected final IContextHubWrapper mContextHubProxy;
- private final ContextHubClientManager mClientManager;
+ protected final ContextHubClientManager mClientManager;
- private final NanoAppStateManager mNanoAppStateManager;
+ protected final NanoAppStateManager mNanoAppStateManager;
- private final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>();
+ @GuardedBy("mTransactionLock")
+ protected final ArrayDeque<ContextHubServiceTransaction> mTransactionQueue = new ArrayDeque<>();
- private final Map<Integer, ContextHubServiceTransaction> mReliableMessageTransactionMap =
+ @GuardedBy("mReliableMessageLock")
+ protected final Map<Integer, ContextHubServiceTransaction> mReliableMessageTransactionMap =
new HashMap<>();
/** A set of host endpoint IDs that have an active pending transaction. */
- private final Set<Short> mReliableMessageHostEndpointIdActiveSet = new HashSet<>();
+ @GuardedBy("mReliableMessageLock")
+ protected final Set<Short> mReliableMessageHostEndpointIdActiveSet = new HashSet<>();
- private final AtomicInteger mNextAvailableId = new AtomicInteger();
+ protected final AtomicInteger mNextAvailableId = new AtomicInteger();
/**
- * The next available message sequence number. We choose a random
- * number to start with to avoid collisions and limit the bound to
- * half of the max value to avoid overflow.
+ * The next available message sequence number. We choose a random number to start with to avoid
+ * collisions and limit the bound to half of the max value to avoid overflow.
*/
- private final AtomicInteger mNextAvailableMessageSequenceNumber =
+ protected final AtomicInteger mNextAvailableMessageSequenceNumber =
new AtomicInteger(new Random().nextInt(Integer.MAX_VALUE / 2));
/*
- * An executor and the future object for scheduling timeout timers and
+ * An executor and the future objects for scheduling timeout timers and
* for scheduling the processing of reliable message transactions.
*/
- private final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(1);
- private ScheduledFuture<?> mTimeoutFuture = null;
- private ScheduledFuture<?> mReliableMessageTransactionFuture = null;
+ protected final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(2);
+
+ @GuardedBy("mTransactionLock")
+ protected ScheduledFuture<?> mTimeoutFuture = null;
+
+ @GuardedBy("mReliableMessageLock")
+ protected ScheduledFuture<?> mReliableMessageTransactionFuture = null;
/*
* The list of previous transaction records.
*/
- private static final int NUM_TRANSACTION_RECORDS = 20;
- private final ConcurrentLinkedEvictingDeque<TransactionRecord> mTransactionRecordDeque =
+ protected static final int NUM_TRANSACTION_RECORDS = 20;
+ protected final ConcurrentLinkedEvictingDeque<TransactionRecord> mTransactionRecordDeque =
new ConcurrentLinkedEvictingDeque<>(NUM_TRANSACTION_RECORDS);
- /**
- * A container class to store a record of transactions.
+ /*
+ * Locks for synchronization of normal transactions separately from reliable message
+ * transactions.
*/
- private class TransactionRecord {
- private final String mTransaction;
- private final long mTimestamp;
+ protected final Object mTransactionLock = new Object();
+ protected final Object mReliableMessageLock = new Object();
+ protected final Object mTransactionRecordLock = new Object();
+
+ /** A container class to store a record of transactions. */
+ protected static class TransactionRecord {
+ protected final String mTransaction;
+ protected final long mTimestamp;
TransactionRecord(String transaction) {
mTransaction = transaction;
@@ -126,8 +142,18 @@ import java.util.concurrent.atomic.AtomicInteger;
}
}
+ /** Used when finishing a transaction. */
+ interface TransactionAcceptConditions {
+ /**
+ * Returns whether to accept the found transaction when receiving a response from the
+ * Context Hub.
+ */
+ boolean acceptTransaction(ContextHubServiceTransaction transaction);
+ }
+
/* package */ ContextHubTransactionManager(
- IContextHubWrapper contextHubProxy, ContextHubClientManager clientManager,
+ IContextHubWrapper contextHubProxy,
+ ContextHubClientManager clientManager,
NanoAppStateManager nanoAppStateManager) {
mContextHubProxy = contextHubProxy;
mClientManager = clientManager;
@@ -409,34 +435,47 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Adds a new transaction to the queue.
- * <p>
- * If there was no pending transaction at the time, the transaction that was added will be
+ *
+ * <p>If there was no pending transaction at the time, the transaction that was added will be
* started in this method. If there were too many transactions in the queue, an exception will
* be thrown.
*
* @param transaction the transaction to add
- * @throws IllegalStateException if the queue is full
*/
/* package */
- synchronized void addTransaction(
- ContextHubServiceTransaction transaction) throws IllegalStateException {
+ void addTransaction(ContextHubServiceTransaction transaction) {
if (transaction == null) {
return;
}
- if (mTransactionQueue.size() >= MAX_PENDING_REQUESTS
- || mReliableMessageTransactionMap.size() >= MAX_PENDING_REQUESTS) {
- throw new IllegalStateException("Transaction queue is full (capacity = "
- + MAX_PENDING_REQUESTS + ")");
+ synchronized (mTransactionRecordLock) {
+ mTransactionRecordDeque.add(new TransactionRecord(transaction.toString()));
}
- mTransactionRecordDeque.add(new TransactionRecord(transaction.toString()));
if (Flags.reliableMessageRetrySupportService()
&& transaction.getTransactionType()
== ContextHubTransaction.TYPE_RELIABLE_MESSAGE) {
- mReliableMessageTransactionMap.put(transaction.getMessageSequenceNumber(), transaction);
+ synchronized (mReliableMessageLock) {
+ if (mReliableMessageTransactionMap.size() >= MAX_PENDING_REQUESTS) {
+ throw new IllegalStateException(
+ "Reliable message transaction queue is full "
+ + "(capacity = "
+ + MAX_PENDING_REQUESTS
+ + ")");
+ }
+ mReliableMessageTransactionMap.put(
+ transaction.getMessageSequenceNumber(), transaction);
+ }
mExecutor.execute(() -> processMessageTransactions());
- } else {
+ return;
+ }
+
+ synchronized (mTransactionLock) {
+ if (mTransactionQueue.size() >= MAX_PENDING_REQUESTS) {
+ throw new IllegalStateException(
+ "Transaction queue is full (capacity = " + MAX_PENDING_REQUESTS + ")");
+ }
+
mTransactionQueue.add(transaction);
if (mTransactionQueue.size() == 1) {
startNextTransaction();
@@ -448,62 +487,85 @@ import java.util.concurrent.atomic.AtomicInteger;
* Handles a transaction response from a Context Hub.
*
* @param transactionId the transaction ID of the response
- * @param success true if the transaction succeeded
+ * @param success true if the transaction succeeded
*/
/* package */
- synchronized void onTransactionResponse(int transactionId, boolean success) {
- ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ void onTransactionResponse(int transactionId, boolean success) {
+ TransactionAcceptConditions conditions =
+ transaction -> transaction.getTransactionId() == transactionId;
+ ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
- Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
- return;
- }
- if (transaction.getTransactionId() != transactionId) {
Log.w(TAG, "Received unexpected transaction response (expected ID = "
- + transaction.getTransactionId() + ", received ID = " + transactionId + ")");
+ + transactionId
+ + ", received ID = "
+ + transaction.getTransactionId()
+ + ")");
return;
}
- transaction.onTransactionComplete(success ? ContextHubTransaction.RESULT_SUCCESS :
- ContextHubTransaction.RESULT_FAILED_AT_HUB);
- removeTransactionAndStartNext();
+ synchronized (transaction) {
+ transaction.onTransactionComplete(
+ success
+ ? ContextHubTransaction.RESULT_SUCCESS
+ : ContextHubTransaction.RESULT_FAILED_AT_HUB);
+ transaction.setComplete();
+ }
}
+ /**
+ * Handles a message delivery response from a Context Hub.
+ *
+ * @param messageSequenceNumber the message sequence number of the response
+ * @param success true if the message was delivered successfully
+ */
/* package */
- synchronized void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
+ void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
if (!Flags.reliableMessageRetrySupportService()) {
- ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ TransactionAcceptConditions conditions =
+ transaction -> transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+ && transaction.getMessageSequenceNumber()
+ == messageSequenceNumber;
+ ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
- Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
+ Log.w(TAG, "Received unexpected message delivery response (expected"
+ + " message sequence number = "
+ + messageSequenceNumber
+ + ", received messageSequenceNumber = "
+ + messageSequenceNumber
+ + ")");
return;
}
- int transactionMessageSequenceNumber = transaction.getMessageSequenceNumber();
- if (transaction.getTransactionType() != ContextHubTransaction.TYPE_RELIABLE_MESSAGE
- || transactionMessageSequenceNumber != messageSequenceNumber) {
- Log.w(TAG, "Received unexpected message transaction response (expected message "
- + "sequence number = "
- + transaction.getMessageSequenceNumber()
- + ", received messageSequenceNumber = " + messageSequenceNumber + ")");
- return;
+ synchronized (transaction) {
+ transaction.onTransactionComplete(
+ success
+ ? ContextHubTransaction.RESULT_SUCCESS
+ : ContextHubTransaction.RESULT_FAILED_AT_HUB);
+ transaction.setComplete();
}
-
- transaction.onTransactionComplete(success ? ContextHubTransaction.RESULT_SUCCESS :
- ContextHubTransaction.RESULT_FAILED_AT_HUB);
- removeTransactionAndStartNext();
return;
}
- ContextHubServiceTransaction transaction =
- mReliableMessageTransactionMap.get(messageSequenceNumber);
- if (transaction == null) {
- Log.w(TAG, "Could not find reliable message transaction with "
- + "message sequence number = "
- + messageSequenceNumber);
- return;
+ ContextHubServiceTransaction transaction = null;
+ synchronized (mReliableMessageLock) {
+ transaction = mReliableMessageTransactionMap.get(messageSequenceNumber);
+ if (transaction == null) {
+ Log.w(
+ TAG,
+ "Could not find reliable message transaction with "
+ + "message sequence number = "
+ + messageSequenceNumber);
+ return;
+ }
+
+ removeMessageTransaction(transaction);
}
- completeMessageTransaction(transaction,
- success ? ContextHubTransaction.RESULT_SUCCESS
+ completeMessageTransaction(
+ transaction,
+ success
+ ? ContextHubTransaction.RESULT_SUCCESS
: ContextHubTransaction.RESULT_FAILED_AT_HUB);
mExecutor.execute(() -> processMessageTransactions());
}
@@ -514,77 +576,116 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param nanoAppStateList the list of nanoapps included in the response
*/
/* package */
- synchronized void onQueryResponse(List<NanoAppState> nanoAppStateList) {
- ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ void onQueryResponse(List<NanoAppState> nanoAppStateList) {
+ TransactionAcceptConditions conditions = transaction ->
+ transaction.getTransactionType() == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
+ ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
- Log.w(TAG, "Received unexpected query response (no transaction pending)");
- return;
- }
- if (transaction.getTransactionType() != ContextHubTransaction.TYPE_QUERY_NANOAPPS) {
Log.w(TAG, "Received unexpected query response (expected " + transaction + ")");
return;
}
- transaction.onQueryResponse(ContextHubTransaction.RESULT_SUCCESS, nanoAppStateList);
- removeTransactionAndStartNext();
+ synchronized (transaction) {
+ transaction.onQueryResponse(ContextHubTransaction.RESULT_SUCCESS, nanoAppStateList);
+ transaction.setComplete();
+ }
}
- /**
- * Handles a hub reset event by stopping a pending transaction and starting the next.
- */
+ /** Handles a hub reset event by stopping a pending transaction and starting the next. */
/* package */
- synchronized void onHubReset() {
+ void onHubReset() {
if (Flags.reliableMessageRetrySupportService()) {
- Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
- mReliableMessageTransactionMap.entrySet().iterator();
- while (iter.hasNext()) {
- completeMessageTransaction(iter.next().getValue(),
- ContextHubTransaction.RESULT_FAILED_AT_HUB, iter);
+ synchronized (mReliableMessageLock) {
+ Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
+ mReliableMessageTransactionMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ removeAndCompleteMessageTransaction(
+ iter.next().getValue(),
+ ContextHubTransaction.RESULT_FAILED_AT_HUB,
+ iter);
+ }
}
}
- ContextHubServiceTransaction transaction = mTransactionQueue.peek();
- if (transaction == null) {
- return;
+ synchronized (mTransactionLock) {
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ if (transaction == null) {
+ return;
+ }
+
+ removeTransactionAndStartNext();
}
+ }
- removeTransactionAndStartNext();
+ /**
+ * This function also starts the next transaction and removes the active transaction from the
+ * queue. The caller should complete the transaction.
+ *
+ * <p>Returns the active transaction if the transaction queue is not empty, the transaction is
+ * not null, and the transaction matches the conditions.
+ */
+ private ContextHubServiceTransaction getTransactionAndHandleNext(
+ TransactionAcceptConditions conditions) {
+ ContextHubServiceTransaction transaction = null;
+ synchronized (mTransactionLock) {
+ transaction = mTransactionQueue.peek();
+ if (transaction == null
+ || (conditions != null && !conditions.acceptTransaction(transaction))) {
+ return null;
+ }
+
+ cancelTimeoutFuture();
+ mTransactionQueue.remove();
+ if (!mTransactionQueue.isEmpty()) {
+ startNextTransaction();
+ }
+ }
+ return transaction;
}
/**
* Pops the front transaction from the queue and starts the next pending transaction request.
- * <p>
- * Removing elements from the transaction queue must only be done through this method. When a
+ *
+ * <p>Removing elements from the transaction queue must only be done through this method. When a
* pending transaction is removed, the timeout timer is cancelled and the transaction is marked
* complete.
- * <p>
- * It is assumed that the transaction queue is non-empty when this method is invoked, and that
- * the caller has obtained a lock on this ContextHubTransactionManager object.
+ *
+ * <p>It is assumed that the transaction queue is non-empty when this method is invoked, and
+ * that the caller has obtained mTransactionLock.
*/
+ @GuardedBy("mTransactionLock")
private void removeTransactionAndStartNext() {
- if (mTimeoutFuture != null) {
- mTimeoutFuture.cancel(/* mayInterruptIfRunning= */ false);
- mTimeoutFuture = null;
- }
+ cancelTimeoutFuture();
ContextHubServiceTransaction transaction = mTransactionQueue.remove();
- transaction.setComplete();
+ synchronized (transaction) {
+ transaction.setComplete();
+ }
if (!mTransactionQueue.isEmpty()) {
startNextTransaction();
}
}
+ /** Cancels the timeout future. */
+ @GuardedBy("mTransactionLock")
+ private void cancelTimeoutFuture() {
+ if (mTimeoutFuture != null) {
+ mTimeoutFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mTimeoutFuture = null;
+ }
+ }
+
/**
* Starts the next pending transaction request.
- * <p>
- * Starting new transactions must only be done through this method. This method continues to
+ *
+ * <p>Starting new transactions must only be done through this method. This method continues to
* process the transaction queue as long as there are pending requests, and no transaction is
* pending.
- * <p>
- * It is assumed that the caller has obtained a lock on this ContextHubTransactionManager
- * object.
+ *
+ * <p>It is assumed that the caller has obtained a lock on mTransactionLock.
*/
+ @GuardedBy("mTransactionLock")
private void startNextTransaction() {
int result = ContextHubTransaction.RESULT_FAILED_UNKNOWN;
while (result != ContextHubTransaction.RESULT_SUCCESS && !mTransactionQueue.isEmpty()) {
@@ -592,28 +693,36 @@ import java.util.concurrent.atomic.AtomicInteger;
result = transaction.onTransact();
if (result == ContextHubTransaction.RESULT_SUCCESS) {
- Runnable onTimeoutFunc = () -> {
- synchronized (this) {
- if (!transaction.isComplete()) {
- Log.d(TAG, transaction + " timed out");
- transaction.onTransactionComplete(
- ContextHubTransaction.RESULT_FAILED_TIMEOUT);
-
- removeTransactionAndStartNext();
- }
- }
- };
+ Runnable onTimeoutFunc =
+ () -> {
+ synchronized (transaction) {
+ if (!transaction.isComplete()) {
+ Log.d(TAG, transaction + " timed out");
+ transaction.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_TIMEOUT);
+ transaction.setComplete();
+ }
+ }
+
+ synchronized (mTransactionLock) {
+ removeTransactionAndStartNext();
+ }
+ };
long timeoutMs = transaction.getTimeout(TimeUnit.MILLISECONDS);
try {
- mTimeoutFuture = mExecutor.schedule(
- onTimeoutFunc, timeoutMs, TimeUnit.MILLISECONDS);
+ mTimeoutFuture =
+ mExecutor.schedule(onTimeoutFunc, timeoutMs, TimeUnit.MILLISECONDS);
} catch (Exception e) {
Log.e(TAG, "Error when schedule a timer", e);
}
} else {
- transaction.onTransactionComplete(
- ContextHubServiceUtil.toTransactionResult(result));
+ synchronized (transaction) {
+ transaction.onTransactionComplete(
+ ContextHubServiceUtil.toTransactionResult(result));
+ transaction.setComplete();
+ }
+
mTransactionQueue.remove();
}
}
@@ -621,81 +730,97 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Processes message transactions, starting and completing them as needed.
- * <p>
- * This function is called when adding a message transaction or when a timer
- * expires for an existing message transaction's retry or timeout. The
- * internal processing loop will iterate at most twice as if one iteration
- * completes a transaction, the next iteration can only start new transactions.
- * If the first iteration does not complete any transaction, the loop will
- * only iterate once.
+ *
+ * <p>This function is called when adding a message transaction or when a timer expires for an
+ * existing message transaction's retry or timeout. The internal processing loop will iterate at
+ * most twice as if one iteration completes a transaction, the next iteration can only start new
+ * transactions. If the first iteration does not complete any transaction, the loop will only
+ * iterate once.
+ *
* <p>
*/
- private synchronized void processMessageTransactions() {
- if (!Flags.reliableMessageRetrySupportService()) {
- return;
- }
-
- if (mReliableMessageTransactionFuture != null) {
- mReliableMessageTransactionFuture.cancel(/* mayInterruptIfRunning= */ false);
- mReliableMessageTransactionFuture = null;
- }
+ private void processMessageTransactions() {
+ synchronized (mReliableMessageLock) {
+ if (!Flags.reliableMessageRetrySupportService()) {
+ return;
+ }
- long now = SystemClock.elapsedRealtimeNanos();
- long nextExecutionTime = Long.MAX_VALUE;
- boolean continueProcessing;
- do {
- continueProcessing = false;
- Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
- mReliableMessageTransactionMap.entrySet().iterator();
- while (iter.hasNext()) {
- ContextHubServiceTransaction transaction = iter.next().getValue();
- short hostEndpointId = transaction.getHostEndpointId();
- int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
- if (numCompletedStartCalls == 0
- && mReliableMessageHostEndpointIdActiveSet.contains(hostEndpointId)) {
- continue;
- }
+ if (mReliableMessageTransactionFuture != null) {
+ mReliableMessageTransactionFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mReliableMessageTransactionFuture = null;
+ }
- long nextRetryTime = transaction.getNextRetryTime();
- long timeoutTime = transaction.getTimeoutTime();
- boolean transactionTimedOut = timeoutTime <= now;
- boolean transactionHitMaxRetries = nextRetryTime <= now
- && numCompletedStartCalls > RELIABLE_MESSAGE_MAX_NUM_RETRY;
- if (transactionTimedOut || transactionHitMaxRetries) {
- completeMessageTransaction(transaction,
- ContextHubTransaction.RESULT_FAILED_TIMEOUT, iter);
- continueProcessing = true;
- } else {
- if (nextRetryTime <= now || numCompletedStartCalls <= 0) {
- startMessageTransaction(transaction, now);
+ long now = SystemClock.elapsedRealtimeNanos();
+ long nextExecutionTime = Long.MAX_VALUE;
+ boolean continueProcessing;
+ do {
+ continueProcessing = false;
+ Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
+ mReliableMessageTransactionMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ ContextHubServiceTransaction transaction = iter.next().getValue();
+ short hostEndpointId = transaction.getHostEndpointId();
+ int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
+ if (numCompletedStartCalls == 0
+ && mReliableMessageHostEndpointIdActiveSet.contains(hostEndpointId)) {
+ continue;
}
- nextExecutionTime = Math.min(nextExecutionTime,
- transaction.getNextRetryTime());
- nextExecutionTime = Math.min(nextExecutionTime,
- transaction.getTimeoutTime());
+ long nextRetryTime = transaction.getNextRetryTime();
+ long timeoutTime = transaction.getTimeoutTime();
+ boolean transactionTimedOut = timeoutTime <= now;
+ boolean transactionHitMaxRetries =
+ nextRetryTime <= now
+ && numCompletedStartCalls > RELIABLE_MESSAGE_MAX_NUM_RETRY;
+ if (transactionTimedOut || transactionHitMaxRetries) {
+ removeAndCompleteMessageTransaction(
+ transaction, ContextHubTransaction.RESULT_FAILED_TIMEOUT, iter);
+ continueProcessing = true;
+ } else {
+ if (nextRetryTime <= now || numCompletedStartCalls <= 0) {
+ startMessageTransaction(transaction, now);
+ }
+
+ nextExecutionTime =
+ Math.min(nextExecutionTime, transaction.getNextRetryTime());
+ nextExecutionTime =
+ Math.min(nextExecutionTime, transaction.getTimeoutTime());
+ }
}
+ } while (continueProcessing);
+
+ if (nextExecutionTime < Long.MAX_VALUE) {
+ mReliableMessageTransactionFuture =
+ mExecutor.schedule(
+ () -> processMessageTransactions(),
+ Math.max(
+ nextExecutionTime - SystemClock.elapsedRealtimeNanos(),
+ RELIABLE_MESSAGE_MIN_WAIT_TIME.toNanos()),
+ TimeUnit.NANOSECONDS);
}
- } while (continueProcessing);
-
- if (nextExecutionTime < Long.MAX_VALUE) {
- mReliableMessageTransactionFuture = mExecutor.schedule(
- () -> processMessageTransactions(),
- Math.max(nextExecutionTime - SystemClock.elapsedRealtimeNanos(),
- RELIABLE_MESSAGE_MIN_WAIT_TIME.toNanos()),
- TimeUnit.NANOSECONDS);
}
}
/**
- * Completes a message transaction and removes it from the reliable message map.
+ * Completes a message transaction.
*
* @param transaction The transaction to complete.
* @param result The result code.
*/
- private void completeMessageTransaction(ContextHubServiceTransaction transaction,
- @ContextHubTransaction.Result int result) {
- completeMessageTransaction(transaction, result, /* iter= */ null);
+ private void completeMessageTransaction(
+ ContextHubServiceTransaction transaction, @ContextHubTransaction.Result int result) {
+ synchronized (transaction) {
+ transaction.onTransactionComplete(result);
+ transaction.setComplete();
+ }
+
+ Log.d(
+ TAG,
+ "Successfully completed reliable message transaction with "
+ + "message sequence number = "
+ + transaction.getMessageSequenceNumber()
+ + " and result = "
+ + result);
}
/**
@@ -705,25 +830,41 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param result The result code.
* @param iter The iterator for the reliable message map - used to remove the message directly.
*/
- private void completeMessageTransaction(ContextHubServiceTransaction transaction,
+ @GuardedBy("mReliableMessageLock")
+ private void removeAndCompleteMessageTransaction(
+ ContextHubServiceTransaction transaction,
@ContextHubTransaction.Result int result,
Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter) {
- transaction.onTransactionComplete(result);
+ removeMessageTransaction(transaction, iter);
+ completeMessageTransaction(transaction, result);
+ }
+ /**
+ * Removes a message transaction from the reliable message map.
+ *
+ * @param transaction The transaction to remove.
+ */
+ @GuardedBy("mReliableMessageLock")
+ private void removeMessageTransaction(ContextHubServiceTransaction transaction) {
+ removeMessageTransaction(transaction, /* iter= */ null);
+ }
+
+ /**
+ * Removes a message transaction from the reliable message map.
+ *
+ * @param transaction The transaction to remove.
+ * @param iter The iterator for the reliable message map - used to remove the message directly.
+ */
+ @GuardedBy("mReliableMessageLock")
+ private void removeMessageTransaction(
+ ContextHubServiceTransaction transaction,
+ Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter) {
if (iter == null) {
mReliableMessageTransactionMap.remove(transaction.getMessageSequenceNumber());
} else {
iter.remove();
}
mReliableMessageHostEndpointIdActiveSet.remove(transaction.getHostEndpointId());
-
- Log.d(
- TAG,
- "Successfully completed reliable message transaction with "
- + "message sequence number = "
- + transaction.getMessageSequenceNumber()
- + " and result = "
- + result);
}
/**
@@ -732,24 +873,25 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param transaction The transaction to start.
* @param now The now time.
*/
+ @GuardedBy("mReliableMessageLock")
private void startMessageTransaction(ContextHubServiceTransaction transaction, long now) {
int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
@ContextHubTransaction.Result int result = transaction.onTransact();
if (result == ContextHubTransaction.RESULT_SUCCESS) {
- Log.d(
- TAG,
- "Successfully "
- + (numCompletedStartCalls == 0 ? "started" : "retried")
- + " reliable message transaction with message sequence number = "
- + transaction.getMessageSequenceNumber());
+ Log.d(
+ TAG,
+ "Successfully "
+ + (numCompletedStartCalls == 0 ? "started" : "retried")
+ + " reliable message transaction with message sequence number = "
+ + transaction.getMessageSequenceNumber());
} else {
- Log.w(
- TAG,
- "Could not start reliable message transaction with "
- + "message sequence number = "
- + transaction.getMessageSequenceNumber()
- + ", result = "
- + result);
+ Log.w(
+ TAG,
+ "Could not start reliable message transaction with "
+ + "message sequence number = "
+ + transaction.getMessageSequenceNumber()
+ + ", result = "
+ + result);
}
transaction.setNextRetryTime(now + RELIABLE_MESSAGE_RETRY_WAIT_TIME.toNanos());
@@ -788,17 +930,19 @@ import java.util.concurrent.atomic.AtomicInteger;
public String toString() {
StringBuilder sb = new StringBuilder();
int i = 0;
- synchronized (this) {
- for (ContextHubServiceTransaction transaction: mTransactionQueue) {
+ synchronized (mTransactionLock) {
+ for (ContextHubServiceTransaction transaction : mTransactionQueue) {
sb.append(i);
sb.append(": ");
sb.append(transaction.toString());
sb.append("\n");
++i;
}
+ }
- if (Flags.reliableMessageRetrySupportService()) {
- for (ContextHubServiceTransaction transaction:
+ if (Flags.reliableMessageRetrySupportService()) {
+ synchronized (mReliableMessageLock) {
+ for (ContextHubServiceTransaction transaction :
mReliableMessageTransactionMap.values()) {
sb.append(i);
sb.append(": ");
@@ -807,7 +951,9 @@ import java.util.concurrent.atomic.AtomicInteger;
++i;
}
}
+ }
+ synchronized (mTransactionRecordLock) {
sb.append("Transaction History:\n");
Iterator<TransactionRecord> iterator = mTransactionRecordDeque.descendingIterator();
while (iterator.hasNext()) {
@@ -815,6 +961,7 @@ import java.util.concurrent.atomic.AtomicInteger;
sb.append("\n");
}
}
+
return sb.toString();
}
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManagerOld.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManagerOld.java
new file mode 100644
index 000000000000..a67fa308a6ea
--- /dev/null
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManagerOld.java
@@ -0,0 +1,475 @@
+/*
+ * 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.server.location.contexthub;
+
+import android.chre.flags.Flags;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppBinary;
+import android.hardware.location.NanoAppMessage;
+import android.hardware.location.NanoAppState;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.time.Duration;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Manages transactions at the Context Hub Service.
+ *
+ * <p>This class maintains a queue of transaction requests made to the ContextHubService by clients,
+ * and executes them through the Context Hub. At any point in time, either the transaction queue is
+ * empty, or there is a pending transaction that is waiting for an asynchronous response from the
+ * hub. This class also handles synchronous errors and timeouts of each transaction.
+ *
+ * <p>This is the old version of ContextHubTransactionManager that uses global synchronization
+ * instead of individual locks. This will be deleted when the
+ * reduce_locking_context_hub_transaction_manager flag is cleaned up.
+ *
+ * @hide
+ */
+/* package */ class ContextHubTransactionManagerOld extends ContextHubTransactionManager {
+ /* package */ ContextHubTransactionManagerOld(
+ IContextHubWrapper contextHubProxy,
+ ContextHubClientManager clientManager,
+ NanoAppStateManager nanoAppStateManager) {
+ super(contextHubProxy, clientManager, nanoAppStateManager);
+ }
+
+ /**
+ * Adds a new transaction to the queue.
+ *
+ * <p>If there was no pending transaction at the time, the transaction that was added will be
+ * started in this method. If there were too many transactions in the queue, an exception will
+ * be thrown.
+ *
+ * @param transaction the transaction to add
+ * @throws IllegalStateException if the queue is full
+ */
+ /* package */
+ @Override
+ synchronized void addTransaction(ContextHubServiceTransaction transaction)
+ throws IllegalStateException {
+ if (transaction == null) {
+ return;
+ }
+
+ if (mTransactionQueue.size() >= MAX_PENDING_REQUESTS
+ || mReliableMessageTransactionMap.size() >= MAX_PENDING_REQUESTS) {
+ throw new IllegalStateException(
+ "Transaction queue is full (capacity = " + MAX_PENDING_REQUESTS + ")");
+ }
+
+ mTransactionRecordDeque.add(new TransactionRecord(transaction.toString()));
+ if (Flags.reliableMessageRetrySupportService()
+ && transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_RELIABLE_MESSAGE) {
+ mReliableMessageTransactionMap.put(transaction.getMessageSequenceNumber(), transaction);
+ mExecutor.execute(() -> processMessageTransactions());
+ } else {
+ mTransactionQueue.add(transaction);
+ if (mTransactionQueue.size() == 1) {
+ startNextTransaction();
+ }
+ }
+ }
+
+ /**
+ * Handles a transaction response from a Context Hub.
+ *
+ * @param transactionId the transaction ID of the response
+ * @param success true if the transaction succeeded
+ */
+ /* package */
+ @Override
+ synchronized void onTransactionResponse(int transactionId, boolean success) {
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ if (transaction == null) {
+ Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
+ return;
+ }
+ if (transaction.getTransactionId() != transactionId) {
+ Log.w(
+ TAG,
+ "Received unexpected transaction response (expected ID = "
+ + transaction.getTransactionId()
+ + ", received ID = "
+ + transactionId
+ + ")");
+ return;
+ }
+
+ transaction.onTransactionComplete(
+ success
+ ? ContextHubTransaction.RESULT_SUCCESS
+ : ContextHubTransaction.RESULT_FAILED_AT_HUB);
+ removeTransactionAndStartNext();
+ }
+
+ /* package */
+ @Override
+ synchronized void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
+ if (!Flags.reliableMessageRetrySupportService()) {
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ if (transaction == null) {
+ Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
+ return;
+ }
+
+ int transactionMessageSequenceNumber = transaction.getMessageSequenceNumber();
+ if (transaction.getTransactionType() != ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+ || transactionMessageSequenceNumber != messageSequenceNumber) {
+ Log.w(
+ TAG,
+ "Received unexpected message transaction response (expected message "
+ + "sequence number = "
+ + transaction.getMessageSequenceNumber()
+ + ", received messageSequenceNumber = "
+ + messageSequenceNumber
+ + ")");
+ return;
+ }
+
+ transaction.onTransactionComplete(
+ success
+ ? ContextHubTransaction.RESULT_SUCCESS
+ : ContextHubTransaction.RESULT_FAILED_AT_HUB);
+ removeTransactionAndStartNext();
+ return;
+ }
+
+ ContextHubServiceTransaction transaction =
+ mReliableMessageTransactionMap.get(messageSequenceNumber);
+ if (transaction == null) {
+ Log.w(
+ TAG,
+ "Could not find reliable message transaction with "
+ + "message sequence number = "
+ + messageSequenceNumber);
+ return;
+ }
+
+ completeMessageTransaction(
+ transaction,
+ success
+ ? ContextHubTransaction.RESULT_SUCCESS
+ : ContextHubTransaction.RESULT_FAILED_AT_HUB);
+ mExecutor.execute(() -> processMessageTransactions());
+ }
+
+ /**
+ * Handles a query response from a Context Hub.
+ *
+ * @param nanoAppStateList the list of nanoapps included in the response
+ */
+ /* package */
+ @Override
+ synchronized void onQueryResponse(List<NanoAppState> nanoAppStateList) {
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ if (transaction == null) {
+ Log.w(TAG, "Received unexpected query response (no transaction pending)");
+ return;
+ }
+ if (transaction.getTransactionType() != ContextHubTransaction.TYPE_QUERY_NANOAPPS) {
+ Log.w(TAG, "Received unexpected query response (expected " + transaction + ")");
+ return;
+ }
+
+ transaction.onQueryResponse(ContextHubTransaction.RESULT_SUCCESS, nanoAppStateList);
+ removeTransactionAndStartNext();
+ }
+
+ /** Handles a hub reset event by stopping a pending transaction and starting the next. */
+ /* package */
+ @Override
+ synchronized void onHubReset() {
+ if (Flags.reliableMessageRetrySupportService()) {
+ Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
+ mReliableMessageTransactionMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ completeMessageTransaction(
+ iter.next().getValue(), ContextHubTransaction.RESULT_FAILED_AT_HUB, iter);
+ }
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ if (transaction == null) {
+ return;
+ }
+
+ removeTransactionAndStartNext();
+ }
+
+ /**
+ * Pops the front transaction from the queue and starts the next pending transaction request.
+ *
+ * <p>Removing elements from the transaction queue must only be done through this method. When a
+ * pending transaction is removed, the timeout timer is cancelled and the transaction is marked
+ * complete.
+ *
+ * <p>It is assumed that the transaction queue is non-empty when this method is invoked, and
+ * that the caller has obtained a lock on this ContextHubTransactionManager object.
+ */
+ private void removeTransactionAndStartNext() {
+ if (mTimeoutFuture != null) {
+ mTimeoutFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mTimeoutFuture = null;
+ }
+
+ ContextHubServiceTransaction transaction = mTransactionQueue.remove();
+ transaction.setComplete();
+
+ if (!mTransactionQueue.isEmpty()) {
+ startNextTransaction();
+ }
+ }
+
+ /**
+ * Starts the next pending transaction request.
+ *
+ * <p>Starting new transactions must only be done through this method. This method continues to
+ * process the transaction queue as long as there are pending requests, and no transaction is
+ * pending.
+ *
+ * <p>It is assumed that the caller has obtained a lock on this ContextHubTransactionManager
+ * object.
+ */
+ private void startNextTransaction() {
+ int result = ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ while (result != ContextHubTransaction.RESULT_SUCCESS && !mTransactionQueue.isEmpty()) {
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ result = transaction.onTransact();
+
+ if (result == ContextHubTransaction.RESULT_SUCCESS) {
+ Runnable onTimeoutFunc =
+ () -> {
+ synchronized (this) {
+ if (!transaction.isComplete()) {
+ Log.d(TAG, transaction + " timed out");
+ transaction.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_TIMEOUT);
+
+ removeTransactionAndStartNext();
+ }
+ }
+ };
+
+ long timeoutMs = transaction.getTimeout(TimeUnit.MILLISECONDS);
+ try {
+ mTimeoutFuture =
+ mExecutor.schedule(onTimeoutFunc, timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ Log.e(TAG, "Error when schedule a timer", e);
+ }
+ } else {
+ transaction.onTransactionComplete(
+ ContextHubServiceUtil.toTransactionResult(result));
+ mTransactionQueue.remove();
+ }
+ }
+ }
+
+ /**
+ * Processes message transactions, starting and completing them as needed.
+ *
+ * <p>This function is called when adding a message transaction or when a timer expires for an
+ * existing message transaction's retry or timeout. The internal processing loop will iterate at
+ * most twice as if one iteration completes a transaction, the next iteration can only start new
+ * transactions. If the first iteration does not complete any transaction, the loop will only
+ * iterate once.
+ *
+ * <p>
+ */
+ private synchronized void processMessageTransactions() {
+ if (!Flags.reliableMessageRetrySupportService()) {
+ return;
+ }
+
+ if (mReliableMessageTransactionFuture != null) {
+ mReliableMessageTransactionFuture.cancel(/* mayInterruptIfRunning= */ false);
+ mReliableMessageTransactionFuture = null;
+ }
+
+ long now = SystemClock.elapsedRealtimeNanos();
+ long nextExecutionTime = Long.MAX_VALUE;
+ boolean continueProcessing;
+ do {
+ continueProcessing = false;
+ Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter =
+ mReliableMessageTransactionMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ ContextHubServiceTransaction transaction = iter.next().getValue();
+ short hostEndpointId = transaction.getHostEndpointId();
+ int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
+ if (numCompletedStartCalls == 0
+ && mReliableMessageHostEndpointIdActiveSet.contains(hostEndpointId)) {
+ continue;
+ }
+
+ long nextRetryTime = transaction.getNextRetryTime();
+ long timeoutTime = transaction.getTimeoutTime();
+ boolean transactionTimedOut = timeoutTime <= now;
+ boolean transactionHitMaxRetries =
+ nextRetryTime <= now
+ && numCompletedStartCalls > RELIABLE_MESSAGE_MAX_NUM_RETRY;
+ if (transactionTimedOut || transactionHitMaxRetries) {
+ completeMessageTransaction(
+ transaction, ContextHubTransaction.RESULT_FAILED_TIMEOUT, iter);
+ continueProcessing = true;
+ } else {
+ if (nextRetryTime <= now || numCompletedStartCalls <= 0) {
+ startMessageTransaction(transaction, now);
+ }
+
+ nextExecutionTime = Math.min(nextExecutionTime, transaction.getNextRetryTime());
+ nextExecutionTime = Math.min(nextExecutionTime, transaction.getTimeoutTime());
+ }
+ }
+ } while (continueProcessing);
+
+ if (nextExecutionTime < Long.MAX_VALUE) {
+ mReliableMessageTransactionFuture =
+ mExecutor.schedule(
+ () -> processMessageTransactions(),
+ Math.max(
+ nextExecutionTime - SystemClock.elapsedRealtimeNanos(),
+ RELIABLE_MESSAGE_MIN_WAIT_TIME.toNanos()),
+ TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /**
+ * Completes a message transaction and removes it from the reliable message map.
+ *
+ * @param transaction The transaction to complete.
+ * @param result The result code.
+ */
+ private void completeMessageTransaction(
+ ContextHubServiceTransaction transaction, @ContextHubTransaction.Result int result) {
+ completeMessageTransaction(transaction, result, /* iter= */ null);
+ }
+
+ /**
+ * Completes a message transaction and removes it from the reliable message map using iter.
+ *
+ * @param transaction The transaction to complete.
+ * @param result The result code.
+ * @param iter The iterator for the reliable message map - used to remove the message directly.
+ */
+ private void completeMessageTransaction(
+ ContextHubServiceTransaction transaction,
+ @ContextHubTransaction.Result int result,
+ Iterator<Map.Entry<Integer, ContextHubServiceTransaction>> iter) {
+ transaction.onTransactionComplete(result);
+
+ if (iter == null) {
+ mReliableMessageTransactionMap.remove(transaction.getMessageSequenceNumber());
+ } else {
+ iter.remove();
+ }
+ mReliableMessageHostEndpointIdActiveSet.remove(transaction.getHostEndpointId());
+
+ Log.d(
+ TAG,
+ "Successfully completed reliable message transaction with "
+ + "message sequence number = "
+ + transaction.getMessageSequenceNumber()
+ + " and result = "
+ + result);
+ }
+
+ /**
+ * Starts a message transaction.
+ *
+ * @param transaction The transaction to start.
+ * @param now The now time.
+ */
+ private void startMessageTransaction(ContextHubServiceTransaction transaction, long now) {
+ int numCompletedStartCalls = transaction.getNumCompletedStartCalls();
+ @ContextHubTransaction.Result int result = transaction.onTransact();
+ if (result == ContextHubTransaction.RESULT_SUCCESS) {
+ Log.d(
+ TAG,
+ "Successfully "
+ + (numCompletedStartCalls == 0 ? "started" : "retried")
+ + " reliable message transaction with message sequence number = "
+ + transaction.getMessageSequenceNumber());
+ } else {
+ Log.w(
+ TAG,
+ "Could not start reliable message transaction with "
+ + "message sequence number = "
+ + transaction.getMessageSequenceNumber()
+ + ", result = "
+ + result);
+ }
+
+ transaction.setNextRetryTime(now + RELIABLE_MESSAGE_RETRY_WAIT_TIME.toNanos());
+ if (transaction.getTimeoutTime() == Long.MAX_VALUE) { // first time starting transaction
+ transaction.setTimeoutTime(now + RELIABLE_MESSAGE_TIMEOUT.toNanos());
+ }
+ transaction.setNumCompletedStartCalls(numCompletedStartCalls + 1);
+ mReliableMessageHostEndpointIdActiveSet.add(transaction.getHostEndpointId());
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ int i = 0;
+ synchronized (this) {
+ for (ContextHubServiceTransaction transaction : mTransactionQueue) {
+ sb.append(i);
+ sb.append(": ");
+ sb.append(transaction.toString());
+ sb.append("\n");
+ ++i;
+ }
+
+ if (Flags.reliableMessageRetrySupportService()) {
+ for (ContextHubServiceTransaction transaction :
+ mReliableMessageTransactionMap.values()) {
+ sb.append(i);
+ sb.append(": ");
+ sb.append(transaction.toString());
+ sb.append("\n");
+ ++i;
+ }
+ }
+
+ sb.append("Transaction History:\n");
+ Iterator<TransactionRecord> iterator = mTransactionRecordDeque.descendingIterator();
+ while (iterator.hasNext()) {
+ sb.append(iterator.next());
+ sb.append("\n");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
index c02b103f1d33..404c8411f750 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
@@ -19,7 +19,6 @@ package com.android.server.locksettings.recoverablekeystore.storage;
import android.annotation.Nullable;
import android.os.Environment;
import android.security.keystore.recovery.KeyChainSnapshot;
-import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -29,9 +28,11 @@ import com.android.server.locksettings.recoverablekeystore.serialization
import com.android.server.locksettings.recoverablekeystore.serialization
.KeyChainSnapshotParserException;
import com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSerializer;
+import com.android.server.utils.Slogf;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
@@ -81,12 +82,14 @@ public class RecoverySnapshotStorage {
public synchronized void put(int uid, KeyChainSnapshot snapshot) {
mSnapshotByUid.put(uid, snapshot);
- try {
- writeToDisk(uid, snapshot);
+ File snapshotFile = getSnapshotFile(uid);
+ try (FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)) {
+ KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
} catch (IOException | CertificateEncodingException e) {
- Log.e(TAG,
- String.format(Locale.US, "Error persisting snapshot for %d to disk", uid),
- e);
+ // If we fail to write the latest snapshot, we should delete any older snapshot that
+ // happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
+ snapshotFile.delete();
+ Slogf.e(TAG, e, "Error persisting snapshot for %d to disk", uid);
}
}
@@ -100,10 +103,17 @@ public class RecoverySnapshotStorage {
return snapshot;
}
- try {
- return readFromDisk(uid);
+ File snapshotFile = getSnapshotFile(uid);
+ try (FileInputStream fileInputStream = new FileInputStream(snapshotFile)) {
+ return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
+ } catch (FileNotFoundException e) {
+ Slogf.i(TAG, "Snapshot for uid %d not found", uid);
+ return null;
} catch (IOException | KeyChainSnapshotParserException e) {
- Log.e(TAG, String.format(Locale.US, "Error reading snapshot for %d from disk", uid), e);
+ // If we fail to read the latest snapshot, we should delete it in case it is in some way
+ // corrupted. We can regenerate snapshots anyway.
+ snapshotFile.delete();
+ Slogf.e(TAG, e, "Error reading snapshot for %d from disk", uid);
return null;
}
}
@@ -116,50 +126,6 @@ public class RecoverySnapshotStorage {
getSnapshotFile(uid).delete();
}
- /**
- * Writes the snapshot for recovery agent {@code uid} to disk.
- *
- * @throws IOException if an IO error occurs writing to disk.
- */
- private void writeToDisk(int uid, KeyChainSnapshot snapshot)
- throws IOException, CertificateEncodingException {
- File snapshotFile = getSnapshotFile(uid);
-
- try (
- FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)
- ) {
- KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
- } catch (IOException | CertificateEncodingException e) {
- // If we fail to write the latest snapshot, we should delete any older snapshot that
- // happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
- snapshotFile.delete();
- throw e;
- }
- }
-
- /**
- * Reads the last snapshot for recovery agent {@code uid} from disk.
- *
- * @return The snapshot, or null if none existed.
- * @throws IOException if an IO error occurs reading from disk.
- */
- @Nullable
- private KeyChainSnapshot readFromDisk(int uid)
- throws IOException, KeyChainSnapshotParserException {
- File snapshotFile = getSnapshotFile(uid);
-
- try (
- FileInputStream fileInputStream = new FileInputStream(snapshotFile)
- ) {
- return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
- } catch (IOException | KeyChainSnapshotParserException e) {
- // If we fail to read the latest snapshot, we should delete it in case it is in some way
- // corrupted. We can regenerate snapshots anyway.
- snapshotFile.delete();
- throw e;
- }
- }
-
private File getSnapshotFile(int uid) {
File folder = getStorageFolder();
String fileName = getSnapshotFileName(uid);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6c2d4f72ce57..88334ebe2abb 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -12577,18 +12577,31 @@ public class NotificationManagerService extends SystemService {
// Checks if this is a request to notify system UI about a notification that
// has been lifetime extended.
- // (We only need to check old for the flag, because in both cancellation and
- // update cases, old should have the flag, whereas in update cases the
- // new will NOT have the flag.)
- // If it is such a request, and this is system UI, we send the post request
- // only to System UI, and break as we don't need to continue checking other
- // Managed Services.
- if (info.isSystemUi() && old != null && old.getNotification() != null
+ // We check both old and new for the flag, to avoid catching updates
+ // (where new will not have the flag).
+ // If it is such a request, and this is the system UI listener, we send
+ // the post request. If it's any other listener, we skip it.
+ if (old != null && old.getNotification() != null
&& (old.getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
+ && sbn != null && sbn.getNotification() != null
+ && (sbn.getNotification().flags
& FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
- final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
- listenerCalls.add(() -> notifyPosted(info, sbnToPost, update));
- break;
+ if (info.isSystemUi()) {
+ final NotificationRankingUpdate update =
+ makeRankingUpdateLocked(info);
+ listenerCalls.add(() -> notifyPosted(info, sbnToPost, update));
+ break;
+ } else {
+ // Skipping because this is the direct-reply "update" and we only
+ // need to send it to sysui, so we immediately continue, before it
+ // can get sent to other listeners below.
+ if (DBG) {
+ Slog.d(TAG, "prepareNotifyPostedLocked: direct reply update, "
+ + "skipping post to " + info.toString());
+ }
+ continue;
+ }
}
}
diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java
index 3bcaf58d0a13..f23d7823be94 100644
--- a/services/core/java/com/android/server/os/NativeTombstoneManager.java
+++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java
@@ -96,12 +96,15 @@ public final class NativeTombstoneManager {
mHandler = thread.getThreadHandler();
mWatcher = new TombstoneWatcher();
- mWatcher.startWatching();
}
void onSystemReady() {
registerForUserRemoval();
registerForPackageRemoval();
+ // TombstoneWatcher depends on DropboxManagerService.
+ // DropboxManagerService started before systemReady.
+ // So it is good to call startWatching here.
+ mWatcher.startWatching();
BootReceiver.initDropboxRateLimiter();
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 4665a72b0b06..b228bb9a89c3 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -5144,10 +5144,28 @@ public class ComputerEngine implements Computer {
}
updateOwnerPackageName = installSource.mUpdateOwnerPackageName;
+
+ if (DEBUG_INSTALL) {
+ Log.d(TAG, "ComputerEngine getInstallSourceInfo updateOwnerPackageName = "
+ + updateOwnerPackageName + ", callingUid = " + callingUid + ", packageName = "
+ + packageName + ", userId = " + userId);
+ }
+
if (updateOwnerPackageName != null) {
final PackageStateInternal ps = mSettings.getPackage(updateOwnerPackageName);
final boolean isCallerSystemOrUpdateOwner = callingUid == Process.SYSTEM_UID
|| isCallerSameApp(updateOwnerPackageName, callingUid);
+
+ if (DEBUG_INSTALL) {
+ Log.d(TAG, "ComputerEngine getInstallSourceInfo ps = "
+ + ps + ", isCallerSystemOrUpdateOwner =" + isCallerSystemOrUpdateOwner
+ + ", isCallerSameApp = "
+ + isCallerSameApp(updateOwnerPackageName, callingUid) + ", filter = "
+ + shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId)
+ + ", FromManagedUserOrProfile = "
+ + isCallerFromManagedUserOrProfile(userId));
+ }
+
// Except for package visibility filtering, we also hide update owner if the installer
// is in the managed user or profile. As we don't enforce the update ownership for the
// managed user and profile, knowing there's an update owner is meaningless in that
@@ -5159,6 +5177,11 @@ public class ComputerEngine implements Computer {
}
}
+ if (DEBUG_INSTALL) {
+ Log.d(TAG, "ComputerEngine getInstallSourceInfo updateOwnerPackageName = "
+ + updateOwnerPackageName);
+ }
+
if (installSource.mIsInitiatingPackageUninstalled) {
// We can't check visibility in the usual way, since the initiating package is no
// longer present. So we apply simpler rules to whether to expose the info:
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 1f79ac0afdac..089bbb7b0022 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -16,7 +16,6 @@
package com.android.server.pm;
-import static android.content.pm.Flags.improveInstallFreeze;
import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED;
import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN;
import static android.content.pm.PackageManager.INSTALL_SCENARIO_DEFAULT;
@@ -1050,13 +1049,13 @@ final class InstallRequest {
}
public void onFreezeStarted() {
- if (mPackageMetrics != null && improveInstallFreeze()) {
+ if (mPackageMetrics != null) {
mPackageMetrics.onStepStarted(PackageMetrics.STEP_FREEZE_INSTALL);
}
}
public void onFreezeCompleted() {
- if (mPackageMetrics != null && improveInstallFreeze()) {
+ if (mPackageMetrics != null) {
mPackageMetrics.onStepFinished(PackageMetrics.STEP_FREEZE_INSTALL);
}
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 897ee4312d22..eb7c24399bba 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -109,6 +109,7 @@ import android.content.pm.PackageInstaller.UserActionReason;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.pm.PackageManagerInternal;
+import android.content.pm.SharedLibraryInfo;
import android.content.pm.SigningDetails;
import android.content.pm.SigningInfo;
import android.content.pm.dex.DexMetadataHelper;
@@ -2880,19 +2881,38 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// since installation is in progress.
activate();
}
+ try {
+ List<PackageInstallerSession> children = getChildSessions();
+ if (isMultiPackage()) {
+ for (PackageInstallerSession child : children) {
+ child.prepareInheritedFiles();
+ child.parseApk();
+ }
+ } else {
+ prepareInheritedFiles();
+ parseApk();
+ }
+ } catch (PackageManagerException e) {
+ final String completeMsg = ExceptionUtils.getCompleteMessage(e);
+ final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg);
+ setSessionFailed(e.error, errorMsg);
+ onSessionVerificationFailure(e.error, errorMsg);
+ }
if (Flags.verificationService()) {
final Supplier<Computer> snapshotSupplier = mPm::snapshotComputer;
if (mVerifierController.isVerifierInstalled(snapshotSupplier, userId)) {
- // TODO: extract shared library declarations
final SigningInfo signingInfo;
+ final List<SharedLibraryInfo> declaredLibraries;
synchronized (mLock) {
signingInfo = new SigningInfo(mSigningDetails);
+ declaredLibraries =
+ mPackageLite == null ? null : mPackageLite.getDeclaredLibraries();
}
// Send the request to the verifier and wait for its response before the rest of
// the installation can proceed.
if (!mVerifierController.startVerificationSession(snapshotSupplier, userId,
- sessionId, params.appPackageName, Uri.fromFile(stageDir), signingInfo,
- /* declaredLibraries= */null, /* extensionParams= */ null,
+ sessionId, getPackageName(), Uri.fromFile(stageDir), signingInfo,
+ declaredLibraries, /* extensionParams= */ null,
new VerifierCallback(), /* retry= */ false)) {
// A verifier is installed but cannot be connected. Installation disallowed.
onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
@@ -2927,12 +2947,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
List<PackageInstallerSession> children = getChildSessions();
if (isMultiPackage()) {
for (PackageInstallerSession child : children) {
- child.prepareInheritedFiles();
- child.parseApkAndExtractNativeLibraries();
+ child.extractNativeLibraries();
}
} else {
- prepareInheritedFiles();
- parseApkAndExtractNativeLibraries();
+ extractNativeLibraries();
}
verifyNonStaged();
} catch (PackageManagerException e) {
@@ -3109,7 +3127,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mStageDirInUse = true;
}
- private void parseApkAndExtractNativeLibraries() throws PackageManagerException {
+ private void parseApk() throws PackageManagerException {
synchronized (mLock) {
if (mStageDirInUse) {
throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
@@ -3142,12 +3160,16 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// stage dir here.
// Besides, PackageLite may be null for staged sessions that don't complete
// pre-reboot verification.
- result = getOrParsePackageLiteLocked(stageDir, /* flags */ 0);
+ mPackageLite = getOrParsePackageLiteLocked(stageDir, /* flags */ 0);
} else {
- result = getOrParsePackageLiteLocked(mResolvedBaseFile, /* flags */ 0);
+ mPackageLite = getOrParsePackageLiteLocked(mResolvedBaseFile, /* flags */ 0);
}
- if (result != null) {
- mPackageLite = result;
+ }
+ }
+
+ private void extractNativeLibraries() throws PackageManagerException {
+ synchronized (mLock) {
+ if (mPackageLite != null) {
if (!isApexSession()) {
synchronized (mProgressLock) {
mInternalProgress = 0.5f;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8bab9de903ba..708e0679d6d4 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1101,7 +1101,7 @@ public class UserManagerService extends IUserManager.Stub {
if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) {
UserManager.invalidateIsUserUnlockedCache();
UserManager.invalidateQuietModeEnabledCache();
- UserManager.invalidateUserSerialNumberCache();
+ UserManager.invalidateCacheOnUserListChange();
}
}
@@ -4448,7 +4448,7 @@ public class UserManagerService extends IUserManager.Stub {
if (userData != null) {
synchronized (mUsersLock) {
- mUsers.put(userData.info.id, userData);
+ addUserDataLU(userData);
if (mNextSerialNumber < 0
|| mNextSerialNumber <= userData.info.id) {
mNextSerialNumber = userData.info.id + 1;
@@ -5724,7 +5724,7 @@ public class UserManagerService extends IUserManager.Stub {
userData.info = userInfo;
userData.userProperties = new UserProperties(
userTypeDetails.getDefaultUserPropertiesReference());
- mUsers.put(userId, userData);
+ addUserDataLU(userData);
}
writeUserLP(userData);
writeUserListLP();
@@ -6138,7 +6138,7 @@ public class UserManagerService extends IUserManager.Stub {
final UserData userData = new UserData();
userData.info = userInfo;
synchronized (mUsersLock) {
- mUsers.put(userInfo.id, userData);
+ addUserDataLU(userData);
}
updateUserIds();
return userData;
@@ -6148,8 +6148,7 @@ public class UserManagerService extends IUserManager.Stub {
@VisibleForTesting
void removeUserInfo(@UserIdInt int userId) {
synchronized (mUsersLock) {
- UserManager.invalidateUserSerialNumberCache();
- mUsers.remove(userId);
+ removeUserDataLU(userId);
}
}
@@ -6579,8 +6578,7 @@ public class UserManagerService extends IUserManager.Stub {
// Remove this user from the list
synchronized (mUsersLock) {
- UserManager.invalidateUserSerialNumberCache();
- mUsers.remove(userId);
+ removeUserDataLU(userId);
mIsUserManaged.delete(userId);
}
synchronized (mUserStates) {
@@ -6969,6 +6967,26 @@ public class UserManagerService extends IUserManager.Stub {
}
/**
+ * Adding user data to mUsers list in one place to invalidate related caches.
+ */
+ @GuardedBy("mUsersLock")
+ private void addUserDataLU(UserData userData) {
+ if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+ UserManager.invalidateCacheOnUserListChange();
+ }
+ mUsers.put(userData.info.id, userData);
+ }
+
+ /**
+ * Removing user data to mUsers list in one place to invalidate related caches.
+ */
+ @GuardedBy("mUsersLock")
+ private void removeUserDataLU(@UserIdInt int userId) {
+ UserManager.invalidateCacheOnUserListChange();
+ mUsers.remove(userId);
+ }
+
+ /**
* Caches the list of user ids in an array, adjusting the array size when necessary.
*/
private void updateUserIds() {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index e47b4c2ee147..ad5c84026aa6 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -731,7 +731,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
KeyEvent.KEYCODE_ASSIST,
KeyEvent.KEYCODE_VOICE_ASSIST,
KeyEvent.KEYCODE_MUTE,
- KeyEvent.KEYCODE_VOLUME_MUTE
+ KeyEvent.KEYCODE_VOLUME_MUTE,
+ KeyEvent.KEYCODE_RECENT_APPS,
+ KeyEvent.KEYCODE_APP_SWITCH,
+ KeyEvent.KEYCODE_NOTIFICATION
));
private static final int MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK = 3;
@@ -2082,12 +2085,21 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
switch (mDoubleTapOnHomeBehavior) {
case DOUBLE_TAP_HOME_RECENT_SYSTEM_UI:
+ if (!isKeyEventForCurrentUser(
+ event.getDisplayId(), event.getKeyCode(), "toggleRecentApps")) {
+ break;
+ }
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH);
mHomeConsumed = true;
toggleRecentApps();
break;
case DOUBLE_TAP_HOME_PIP_MENU:
+ if (!isKeyEventForCurrentUser(
+ event.getDisplayId(), event.getKeyCode(),
+ "showPictureInPictureMenu")) {
+ break;
+ }
mHomeConsumed = true;
showPictureInPictureMenuInternal();
break;
@@ -2116,12 +2128,20 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
break;
case LONG_PRESS_HOME_ASSIST:
+ if (!isKeyEventForCurrentUser(
+ event.getDisplayId(), event.getKeyCode(), "launchAssistAction")) {
+ break;
+ }
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT);
launchAssistAction(null, event.getDeviceId(), event.getEventTime(),
AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
break;
case LONG_PRESS_HOME_NOTIFICATION_PANEL:
+ if (!isKeyEventForCurrentUser(
+ event.getDisplayId(), event.getKeyCode(), "toggleNotificationPanel")) {
+ break;
+ }
notifyKeyGestureCompleted(event,
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL);
toggleNotificationPanel();
@@ -3497,7 +3517,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (isUserSetupComplete() && !keyguardOn) {
if (mModifierShortcutManager.interceptKey(event)) {
- dismissKeyboardShortcutsMenu();
+ if (isKeyEventForCurrentUser(
+ event.getDisplayId(), event.getKeyCode(),
+ "dismissKeyboardShortcutsMenu")) {
+ dismissKeyboardShortcutsMenu();
+ }
mPendingMetaAction = false;
mPendingCapsLockToggle = false;
return true;
@@ -4820,7 +4844,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
// no keyguard stuff to worry about, just launch home!
- if (mRecentsVisible) {
+ // If Recents is visible and the action is not from visible background users,
+ // hide Recents and notify it to launch Home.
+ if (mRecentsVisible
+ && (!mVisibleBackgroundUsersEnabled || displayId == DEFAULT_DISPLAY)) {
try {
ActivityManager.getService().stopAppSwitches();
} catch (RemoteException e) {}
@@ -5570,6 +5597,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
* Notify the StatusBar that a system key was pressed.
*/
private void sendSystemKeyToStatusBar(KeyEvent key) {
+ if (!isKeyEventForCurrentUser(key.getDisplayId(), key.getKeyCode(), "handleSystemKey")) {
+ return;
+ }
IStatusBarService statusBar = getStatusBarService();
if (statusBar != null) {
try {
diff --git a/services/core/java/com/android/server/search/SearchManagerService.java b/services/core/java/com/android/server/search/SearchManagerService.java
index 9b39fa1e177c..a49a9fdf4cca 100644
--- a/services/core/java/com/android/server/search/SearchManagerService.java
+++ b/services/core/java/com/android/server/search/SearchManagerService.java
@@ -46,6 +46,7 @@ import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemService.TargetUser;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
import java.io.FileDescriptor;
@@ -89,6 +90,8 @@ public class SearchManagerService extends ISearchManager.Stub {
@GuardedBy("mSearchables")
private final SparseArray<Searchables> mSearchables = new SparseArray<>();
+ private final UserManagerInternal mUserManagerInternal;
+
/**
* Initializes the Search Manager service in the provided system context.
* Only one instance of this object should be created!
@@ -101,6 +104,7 @@ public class SearchManagerService extends ISearchManager.Stub {
mMyPackageMonitor.register(context, null, UserHandle.ALL, true);
new GlobalSearchProviderObserver(context.getContentResolver());
mHandler = BackgroundThread.getHandler();
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
}
private Searchables getSearchables(int userId) {
@@ -336,6 +340,14 @@ public class SearchManagerService extends ISearchManager.Stub {
@Override
public void launchAssist(int userHandle, Bundle args) {
+ // Currently, visible background users are not allowed to launch assist.(b/332222893)
+ // TODO(b/368715893): Consider indirect calls from system service when checking the
+ // calling user.
+ final int callingUserId = UserHandle.getCallingUserId();
+ if (mUserManagerInternal.isVisibleBackgroundFullUser(callingUserId)) {
+ throw new SecurityException("Visible background user(u" + callingUserId
+ + ") is not permitted to launch assist.");
+ }
StatusBarManagerInternal statusBarManager =
LocalServices.getService(StatusBarManagerInternal.class);
if (statusBarManager != null) {
diff --git a/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java b/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java
index 0d420a535415..dcb47a7b60b6 100644
--- a/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java
+++ b/services/core/java/com/android/server/stats/bootstrap/StatsBootstrapAtomService.java
@@ -62,6 +62,9 @@ public class StatsBootstrapAtomService extends IStatsBootstrapAtomService.Stub {
case StatsBootstrapAtomValue.bytesValue:
builder.writeByteArray(value.getBytesValue());
break;
+ case StatsBootstrapAtomValue.stringArrayValue:
+ builder.writeStringArray(value.getStringArrayValue());
+ break;
default:
Slog.e(TAG, "Unexpected value type " + value.getTag()
+ " when logging atom " + atom.atomId);
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index fb5c1154c7f0..3f814f9db1c8 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -244,7 +244,7 @@ class ActivityMetricsLogger {
status = ":completed-same-process:";
} else {
if (endInfo.mTransitionType == TYPE_TRANSITION_HOT_LAUNCH) {
- status = ":completed-hot:";
+ status = !endInfo.mRelaunched ? ":completed-hot:" : ":completed-relaunch:";
} else if (endInfo.mTransitionType == TYPE_TRANSITION_WARM_LAUNCH) {
status = ":completed-warm:";
} else {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index c7fafe929d7b..a44eb48d7836 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -33,7 +33,6 @@ import static android.app.ActivityOptions.ANIM_UNDEFINED;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OP_PICTURE_IN_PICTURE;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.WaitResult.INVALID_DELAY;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
@@ -42,7 +41,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -8394,15 +8392,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
// and back which can cause visible issues (see b/184078928).
final int parentWindowingMode =
newParentConfiguration.windowConfiguration.getWindowingMode();
- final boolean isInCameraCompatFreeform = parentWindowingMode == WINDOWING_MODE_FREEFORM
- && mAppCompatController.getAppCompatCameraOverrides().getFreeformCameraCompatMode()
- != CAMERA_COMPAT_FREEFORM_NONE;
// Bubble activities should always fill their parent and should not be letterboxed.
final boolean isFixedOrientationLetterboxAllowed = !getLaunchedFromBubble()
&& (parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
|| parentWindowingMode == WINDOWING_MODE_FULLSCREEN
- || isInCameraCompatFreeform
+ || AppCompatCameraPolicy.shouldCameraCompatControlOrientation(this)
// When starting to switch between PiP and fullscreen, the task is pinned
// and the activity is fullscreen. But only allow to apply letterbox if the
// activity is exiting PiP because an entered PiP should fill the task.
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 35ec5adf54b0..0580d4a5a4a3 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -43,7 +43,6 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Binder;
-import android.os.Bundle;
import android.os.IBinder;
import android.os.Trace;
import android.os.UserHandle;
@@ -550,14 +549,14 @@ public class ActivityStartController {
* Starts an activity in the TaskFragment.
* @param taskFragment TaskFragment {@link TaskFragment} to start the activity in.
* @param activityIntent intent to start the activity.
- * @param activityOptions ActivityOptions to start the activity with.
+ * @param activityOptions SafeActivityOptions to start the activity with.
* @param resultTo the caller activity
* @param callingUid the caller uid
* @param callingPid the caller pid
* @return the start result.
*/
int startActivityInTaskFragment(@NonNull TaskFragment taskFragment,
- @NonNull Intent activityIntent, @Nullable Bundle activityOptions,
+ @NonNull Intent activityIntent, @Nullable SafeActivityOptions activityOptions,
@Nullable IBinder resultTo, int callingUid, int callingPid,
@Nullable IBinder errorCallbackToken) {
final ActivityRecord caller =
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 670a61dca5c8..05dcbb7f9af4 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
@@ -268,7 +269,16 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> {
}
final DisplayPolicy.DecorInsets.Info decor =
displayContent.getDisplayPolicy().getDecorInsetsInfo(rotation, dw, dh);
- outAppBounds.intersectUnchecked(decor.mOverrideNonDecorFrame);
+ if (!outAppBounds.intersect(decor.mOverrideNonDecorFrame)) {
+ // TODO (b/364883053): When a split screen is requested from an app intent for a new
+ // task, the bounds is not the final bounds, and this is also not a bounds change
+ // event handled correctly with the offset. Revert back to legacy method for this
+ // case.
+ if (inOutConfig.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_MULTI_WINDOW) {
+ outAppBounds.inset(decor.mOverrideNonDecorInsets);
+ }
+ }
if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
outAppBounds.offset(-task.mOffsetXForInsets, -task.mOffsetYForInsets);
}
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index bc33946ef618..0b5872b3e601 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -285,6 +285,11 @@ final class ContentRecorder implements WindowContainerListener {
}
}
+ private boolean isDisplayReadyForMirroring() {
+ return mDisplayContent.getDisplayInfo().type != Display.TYPE_EXTERNAL
+ || mDisplayContent.mWmService.mDisplayManagerInternal.isDisplayReadyForMirroring(
+ mDisplayContent.getDisplayId());
+ }
/**
* Ensure recording does not fall back to the display stack; ensure the recording is stopped
@@ -335,7 +340,7 @@ final class ContentRecorder implements WindowContainerListener {
return;
}
- if (mContentRecordingSession.isWaitingForConsent()) {
+ if (mContentRecordingSession.isWaitingForConsent() || !isDisplayReadyForMirroring()) {
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: waiting to record, so do "
+ "nothing");
return;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 266fdbba0be7..41a580438776 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1407,7 +1407,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
displayId = rootTask != null ? rootTask.getDisplayId() : DEFAULT_DISPLAY;
}
- final DisplayContent display = getDisplayContent(displayId);
+ final DisplayContent display = getDisplayContentOrCreate(displayId);
return display.reduceOnAllTaskDisplayAreas((taskDisplayArea, result) ->
result | startHomeOnTaskDisplayArea(userId, reason, taskDisplayArea,
allowInstrumenting, fromHomeKey),
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 1659f7bc6eed..585537b318dd 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -547,13 +547,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
if (!ar.isVisible() || !ar.isVisibleRequested()) return;
if (mConfigAtEndActivities == null) {
mConfigAtEndActivities = new ArrayList<>();
- }
- if (mConfigAtEndActivities.contains(ar)) {
+ } else if (mConfigAtEndActivities.contains(ar)) {
return;
}
mConfigAtEndActivities.add(ar);
ar.pauseConfigurationDispatch();
- snapshotStartState(ar);
+ collect(ar);
mChanges.get(ar).mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END;
});
}
@@ -1705,54 +1704,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
change.mFlags |= ChangeInfo.FLAG_CHANGE_NO_ANIMATION;
}
- void prepareConfigAtEnd(SurfaceControl.Transaction transact, ArrayList<ChangeInfo> targets) {
- if (mConfigAtEndActivities == null) return;
- for (int i = 0; i < mConfigAtEndActivities.size(); ++i) {
- final ActivityRecord ar = mConfigAtEndActivities.get(i);
- if (!ar.isVisibleRequested()) continue;
- final SurfaceControl sc = ar.getSurfaceControl();
- if (sc == null) continue;
- final Task task = ar.getTask();
- if (task == null) continue;
- // If task isn't animating, then it means shell is animating activity directly (within
- // task), so don't do any setup.
- if (!containsChangeFor(task, targets)) continue;
- final ChangeInfo change = mChanges.get(ar);
- final Rect startBounds = change.mAbsoluteBounds;
- Rect hintRect = null;
- if (ar.getWindowingMode() == WINDOWING_MODE_PINNED && ar.pictureInPictureArgs != null
- && ar.pictureInPictureArgs.getSourceRectHint() != null) {
- hintRect = ar.pictureInPictureArgs.getSourceRectHint();
- }
- if (hintRect == null) {
- hintRect = new Rect(startBounds);
- hintRect.offsetTo(0, 0);
- }
- final Rect endBounds = ar.getBounds();
- final Rect taskEndBounds = task.getBounds();
- // FA = final activity bounds (absolute)
- // FT = final task bounds (absolute)
- // SA = start activity bounds (absolute)
- // H = source hint (relative to start activity bounds)
- // We want to transform the activity so that when the task is at FT, H overlaps with FA
-
- // This scales the activity such that the hint rect has the same dimensions
- // as the final activity bounds.
- float hintToEndScaleX = ((float) endBounds.width()) / ((float) hintRect.width());
- float hintToEndScaleY = ((float) endBounds.height()) / ((float) hintRect.height());
- // top-left needs to be (FA.tl - FT.tl) - H.tl * hintToEnd . H is relative to the
- // activity; so, for example, if shrinking H to FA (hintToEnd < 1), then the tl of the
- // shrunk SA is closer to H than expected, so we need to reduce how much we offset SA
- // to get H.tl to match.
- float startActPosInTaskEndX =
- (endBounds.left - taskEndBounds.left) - hintRect.left * hintToEndScaleX;
- float startActPosInTaskEndY =
- (endBounds.top - taskEndBounds.top) - hintRect.top * hintToEndScaleY;
- transact.setScale(sc, hintToEndScaleX, hintToEndScaleY);
- transact.setPosition(sc, startActPosInTaskEndX, startActPosInTaskEndY);
- }
- }
-
static boolean containsChangeFor(WindowContainer wc, ArrayList<ChangeInfo> list) {
for (int i = list.size() - 1; i >= 0; --i) {
if (list.get(i).mContainer == wc) return true;
@@ -1833,7 +1784,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
// Resolve the animating targets from the participants.
mTargets = calculateTargets(mParticipants, mChanges);
- prepareConfigAtEnd(transaction, mTargets);
// Check whether the participants were animated from back navigation.
mController.mAtm.mBackNavigationController.onTransactionReady(this, mTargets,
@@ -2669,6 +2619,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
if (reportIfNotTop(target)) {
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
" keep as target %s", target);
+ } else if ((targetChange.mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) != 0) {
+ // config-at-end activities do not match the end-state, so they should be treated
+ // as independent.
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
+ " keep as cfg-at-end target %s", target);
} else {
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
" remove from targets %s", target);
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 2229807f5db1..82c7a9350eca 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1523,8 +1523,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
final IBinder callerActivityToken = operation.getActivityToken();
final Intent activityIntent = operation.getActivityIntent();
final Bundle activityOptions = operation.getBundle();
+ final SafeActivityOptions safeOptions =
+ SafeActivityOptions.fromBundle(activityOptions, caller.mPid, caller.mUid);
final int result = waitAsyncStart(() -> mService.getActivityStartController()
- .startActivityInTaskFragment(taskFragment, activityIntent, activityOptions,
+ .startActivityInTaskFragment(taskFragment, activityIntent, safeOptions,
callerActivityToken, caller.mUid, caller.mPid,
errorCallbackToken));
if (!isStartResultSuccessful(result)) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index efca90217e83..248ed1a58b75 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -337,6 +337,8 @@ public:
int32_t getMousePointerSpeed();
void setPointerSpeed(int32_t speed);
void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled);
+ void setMouseReverseVerticalScrollingEnabled(bool enabled);
+ void setMouseSwapPrimaryButtonEnabled(bool enabled);
void setTouchpadPointerSpeed(int32_t speed);
void setTouchpadNaturalScrollingEnabled(bool enabled);
void setTouchpadTapToClickEnabled(bool enabled);
@@ -482,6 +484,12 @@ private:
// True if stylus button reporting through motion events is enabled.
bool stylusButtonMotionEventsEnabled{true};
+ // True if mouse vertical scrolling is reversed.
+ bool mouseReverseVerticalScrollingEnabled{false};
+
+ // True if the mouse primary button is swapped (left/right buttons).
+ bool mouseSwapPrimaryButtonEnabled{false};
+
// The touchpad pointer speed, as a number from -7 (slowest) to 7 (fastest).
int32_t touchpadPointerSpeed{0};
@@ -762,6 +770,10 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon
outConfig->defaultPointerDisplayId = mLocked.pointerDisplayId;
+ outConfig->mouseReverseVerticalScrollingEnabled =
+ mLocked.mouseReverseVerticalScrollingEnabled;
+ outConfig->mouseSwapPrimaryButtonEnabled = mLocked.mouseSwapPrimaryButtonEnabled;
+
outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed;
outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled;
outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled;
@@ -1317,6 +1329,36 @@ int32_t NativeInputManager::getMousePointerSpeed() {
return mLocked.pointerSpeed;
}
+void NativeInputManager::setMouseReverseVerticalScrollingEnabled(bool enabled) {
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ if (mLocked.mouseReverseVerticalScrollingEnabled == enabled) {
+ return;
+ }
+
+ mLocked.mouseReverseVerticalScrollingEnabled = enabled;
+ } // release lock
+
+ mInputManager->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::Change::MOUSE_SETTINGS);
+}
+
+void NativeInputManager::setMouseSwapPrimaryButtonEnabled(bool enabled) {
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ if (mLocked.mouseSwapPrimaryButtonEnabled == enabled) {
+ return;
+ }
+
+ mLocked.mouseSwapPrimaryButtonEnabled = enabled;
+ } // release lock
+
+ mInputManager->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::Change::MOUSE_SETTINGS);
+}
+
void NativeInputManager::setPointerSpeed(int32_t speed) {
{ // acquire lock
std::scoped_lock _l(mLock);
@@ -3002,6 +3044,18 @@ static jint nativeGetLastUsedInputDeviceId(JNIEnv* env, jobject nativeImplObj) {
return static_cast<jint>(im->getInputManager()->getReader().getLastUsedInputDeviceId());
}
+static void nativeSetMouseReverseVerticalScrollingEnabled(JNIEnv* env, jobject nativeImplObj,
+ bool enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->setMouseReverseVerticalScrollingEnabled(enabled);
+}
+
+static void nativeSetMouseSwapPrimaryButtonEnabled(JNIEnv* env, jobject nativeImplObj,
+ bool enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->setMouseSwapPrimaryButtonEnabled(enabled);
+}
+
// ----------------------------------------------------------------------------
static const JNINativeMethod gInputManagerMethods[] = {
@@ -3048,6 +3102,9 @@ static const JNINativeMethod gInputManagerMethods[] = {
{"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
{"setMousePointerAccelerationEnabled", "(IZ)V",
(void*)nativeSetMousePointerAccelerationEnabled},
+ {"setMouseReverseVerticalScrollingEnabled", "(Z)V",
+ (void*)nativeSetMouseReverseVerticalScrollingEnabled},
+ {"setMouseSwapPrimaryButtonEnabled", "(Z)V", (void*)nativeSetMouseSwapPrimaryButtonEnabled},
{"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
{"setTouchpadNaturalScrollingEnabled", "(Z)V",
(void*)nativeSetTouchpadNaturalScrollingEnabled},
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 2836d46b0f1a..2add5b09f15b 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -349,7 +349,7 @@ class AnrTimerTracer {
return nullptr;
}
- // Return the currently watched pids. The lock must be held.
+ // Return the currently watched pids as a comma-separated list. The lock must be held.
std::string watchedPidsLocked() const {
if (watched_.size() == 0) return "none";
bool first = true;
@@ -357,6 +357,7 @@ class AnrTimerTracer {
for (auto i = watched_.cbegin(); i != watched_.cend(); i++) {
if (first) {
result += StringPrintf("%d", *i);
+ first = false;
} else {
result += StringPrintf(",%d", *i);
}
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 776de2e52061..20c69ac93f63 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -464,7 +464,7 @@
<xs:annotation name="nonnull"/>
<xs:annotation name="final"/>
</xs:element>
- <xs:element name="customAnimationRateSec" type="nonNegativeDecimal" minOccurs="0" maxOccurs="1">
+ <xs:element name="customAnimationRate" type="nonNegativeDecimal" minOccurs="0" maxOccurs="1">
<xs:annotation name="nonnull"/>
<xs:annotation name="final"/>
</xs:element>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 110a5a20da6a..a8f18b3d2eee 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -345,12 +345,12 @@ package com.android.server.display.config {
public class PowerThrottlingConfig {
ctor public PowerThrottlingConfig();
method @NonNull public final java.math.BigDecimal getBrightnessLowestCapAllowed();
- method @NonNull public final java.math.BigDecimal getCustomAnimationRateSec();
+ method @NonNull public final java.math.BigDecimal getCustomAnimationRate();
method @NonNull public final java.math.BigInteger getPollingWindowMaxMillis();
method @NonNull public final java.math.BigInteger getPollingWindowMinMillis();
method public final java.util.List<com.android.server.display.config.PowerThrottlingMap> getPowerThrottlingMap();
method public final void setBrightnessLowestCapAllowed(@NonNull java.math.BigDecimal);
- method public final void setCustomAnimationRateSec(@NonNull java.math.BigDecimal);
+ method public final void setCustomAnimationRate(@NonNull java.math.BigDecimal);
method public final void setPollingWindowMaxMillis(@NonNull java.math.BigInteger);
method public final void setPollingWindowMinMillis(@NonNull java.math.BigInteger);
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 2be999fc84e0..7e450dd965d6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -11874,75 +11874,51 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
throw new IllegalArgumentException("Invalid package name: " + validationResult);
}
- if (Flags.setApplicationRestrictionsCoexistence()) {
- EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
- who,
- MANAGE_DEVICE_POLICY_APP_RESTRICTIONS,
- caller.getPackageName(),
- caller.getUserId()
- );
-
+ final boolean isRoleHolder;
+ if (who != null) {
+ // DO or PO
+ Preconditions.checkCallAuthorization(
+ (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
+ Preconditions.checkCallAuthorization(!parent,
+ "DO or PO cannot call this on parent");
+ // Caller has opted to be treated as DPC (by passing a non-null who), so don't
+ // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
+ isRoleHolder = false;
+ } else {
+ // Delegates, or the DMRH. Only DMRH can call this on COPE parent
+ isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
+ if (parent) {
+ Preconditions.checkCallAuthorization(isRoleHolder);
+ Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
+ "Role Holder can only operate parent app restriction on COPE devices");
+ } else {
+ Preconditions.checkCallAuthorization(isRoleHolder
+ || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
+ }
+ }
+ // DMRH caller uses policy engine, others still use legacy code path
+ if (isRoleHolder) {
+ EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
+ caller.getPackageName());
+ int affectedUserId = parent
+ ? getProfileParentId(caller.getUserId()) : caller.getUserId();
if (restrictions == null || restrictions.isEmpty()) {
mDevicePolicyEngine.removeLocalPolicy(
PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
enforcingAdmin,
- caller.getUserId());
+ affectedUserId);
} else {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
enforcingAdmin,
new BundlePolicyValue(restrictions),
- caller.getUserId());
+ affectedUserId);
}
- setBackwardsCompatibleAppRestrictions(
- caller, packageName, restrictions, caller.getUserHandle());
} else {
- final boolean isRoleHolder;
- if (who != null) {
- // DO or PO
- Preconditions.checkCallAuthorization(
- (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
- Preconditions.checkCallAuthorization(!parent,
- "DO or PO cannot call this on parent");
- // Caller has opted to be treated as DPC (by passing a non-null who), so don't
- // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
- isRoleHolder = false;
- } else {
- // Delegates, or the DMRH. Only DMRH can call this on COPE parent
- isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
- if (parent) {
- Preconditions.checkCallAuthorization(isRoleHolder);
- Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
- "Role Holder can only operate parent app restriction on COPE devices");
- } else {
- Preconditions.checkCallAuthorization(isRoleHolder
- || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
- }
- }
- // DMRH caller uses policy engine, others still use legacy code path
- if (isRoleHolder) {
- EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
- caller.getPackageName());
- int affectedUserId = parent
- ? getProfileParentId(caller.getUserId()) : caller.getUserId();
- if (restrictions == null || restrictions.isEmpty()) {
- mDevicePolicyEngine.removeLocalPolicy(
- PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- enforcingAdmin,
- affectedUserId);
- } else {
- mDevicePolicyEngine.setLocalPolicy(
- PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- enforcingAdmin,
- new BundlePolicyValue(restrictions),
- affectedUserId);
- }
- } else {
- mInjector.binderWithCleanCallingIdentity(() -> {
- mUserManager.setApplicationRestrictions(packageName, restrictions,
- caller.getUserHandle());
- });
- }
+ mInjector.binderWithCleanCallingIdentity(() -> {
+ mUserManager.setApplicationRestrictions(packageName, restrictions,
+ caller.getUserHandle());
+ });
}
DevicePolicyEventLogger
@@ -11953,31 +11929,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
.write();
}
- /**
- * Set app restrictions in user manager for DPC callers only to keep backwards compatibility
- * for the old getApplicationRestrictions API.
- */
- private void setBackwardsCompatibleAppRestrictions(
- CallerIdentity caller, String packageName, Bundle restrictions, UserHandle userHandle) {
- if ((caller.hasAdminComponent() && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)))
- || (caller.hasPackage() && isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS))) {
- Bundle restrictionsToApply = restrictions == null || restrictions.isEmpty()
- ? getAppRestrictionsSetByAnyAdmin(packageName, userHandle)
- : restrictions;
- mInjector.binderWithCleanCallingIdentity(() -> {
- mUserManager.setApplicationRestrictions(packageName, restrictionsToApply,
- userHandle);
- });
- } else {
- // Notify package of changes via an intent - only sent to explicitly registered
- // receivers. Sending here because For DPCs, this is being sent in UMS.
- final Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
- changeIntent.setPackage(packageName);
- changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
- mContext.sendBroadcastAsUser(changeIntent, userHandle);
- }
- }
-
private Bundle getAppRestrictionsSetByAnyAdmin(String packageName, UserHandle userHandle) {
LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
@@ -13257,68 +13208,47 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
String packageName, boolean parent) {
final CallerIdentity caller = getCallerIdentity(who, callerPackage);
- // IMPORTANT: The code behind the if branch is OUTDATED and requires additional work before
- // enabling the feature flag below.
- // TODO(b/369141952): Update DPM.getApplicationRestrictions coexistence code
- if (Flags.setApplicationRestrictionsCoexistence()) {
- EnforcingAdmin enforcingAdmin = enforceCanQueryAndGetEnforcingAdmin(
- who,
- MANAGE_DEVICE_POLICY_APP_RESTRICTIONS,
- caller.getPackageName(),
- caller.getUserId()
- );
-
+ final boolean isRoleHolder;
+ if (who != null) {
+ // Caller is DO or PO. They cannot call this on parent
+ Preconditions.checkCallAuthorization(!parent
+ && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
+ // Caller has opted to be treated as DPC (by passing a non-null who), so don't
+ // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
+ isRoleHolder = false;
+ } else {
+ // Caller is delegates or the DMRH. Only DMRH can call this on parent
+ isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
+ if (parent) {
+ Preconditions.checkCallAuthorization(isRoleHolder);
+ Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
+ "Role Holder can only operate parent app restriction on COPE devices");
+ } else {
+ Preconditions.checkCallAuthorization(isRoleHolder
+ || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
+ }
+ }
+ if (isRoleHolder) {
+ EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
+ caller.getPackageName());
+ int affectedUserId = parent
+ ? getProfileParentId(caller.getUserId()) : caller.getUserId();
LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- caller.getUserId());
- if (policies.isEmpty() || !policies.containsKey(enforcingAdmin)) {
+ affectedUserId);
+ if (!policies.containsKey(enforcingAdmin)) {
return Bundle.EMPTY;
}
return policies.get(enforcingAdmin).getValue();
} else {
- final boolean isRoleHolder;
- if (who != null) {
- // Caller is DO or PO. They cannot call this on parent
- Preconditions.checkCallAuthorization(!parent
- && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)));
- // Caller has opted to be treated as DPC (by passing a non-null who), so don't
- // consider it as the DMRH, even if the caller is both the DPC and the DMRH.
- isRoleHolder = false;
- } else {
- // Caller is delegates or the DMRH. Only DMRH can call this on parent
- isRoleHolder = isCallerDevicePolicyManagementRoleHolder(caller);
- if (parent) {
- Preconditions.checkCallAuthorization(isRoleHolder);
- Preconditions.checkState(isOrganizationOwnedDeviceWithManagedProfile(),
- "Role Holder can only operate parent app restriction on COPE devices");
- } else {
- Preconditions.checkCallAuthorization(isRoleHolder
- || isCallerDelegate(caller, DELEGATION_APP_RESTRICTIONS));
- }
- }
- if (isRoleHolder) {
- EnforcingAdmin enforcingAdmin = getEnforcingAdminForCaller(/* who */ null,
- caller.getPackageName());
- int affectedUserId = parent
- ? getProfileParentId(caller.getUserId()) : caller.getUserId();
- LinkedHashMap<EnforcingAdmin, PolicyValue<Bundle>> policies =
- mDevicePolicyEngine.getLocalPoliciesSetByAdmins(
- PolicyDefinition.APPLICATION_RESTRICTIONS(packageName),
- affectedUserId);
- if (!policies.containsKey(enforcingAdmin)) {
- return Bundle.EMPTY;
- }
- return policies.get(enforcingAdmin).getValue();
- } else {
- return mInjector.binderWithCleanCallingIdentity(() -> {
- Bundle bundle = mUserManager.getApplicationRestrictions(packageName,
- caller.getUserHandle());
- // if no restrictions were saved, mUserManager.getApplicationRestrictions
- // returns null, but DPM method should return an empty Bundle as per JavaDoc
- return bundle != null ? bundle : Bundle.EMPTY;
- });
- }
+ return mInjector.binderWithCleanCallingIdentity(() -> {
+ Bundle bundle = mUserManager.getApplicationRestrictions(packageName,
+ caller.getUserHandle());
+ // if no restrictions were saved, mUserManager.getApplicationRestrictions
+ // returns null, but DPM method should return an empty Bundle as per JavaDoc
+ return bundle != null ? bundle : Bundle.EMPTY;
+ });
}
}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
index bc64e158e830..96fb4535b992 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -34,7 +34,6 @@ import android.util.ArraySet
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.infra.AndroidFuture
-import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.atomic.AtomicBoolean
import org.junit.Test
@@ -392,6 +391,10 @@ class MetadataSyncAdapterTest {
return AndroidFuture.completedFuture(mutableListOf())
}
}
+
+ override fun close() {
+ Log.d("FakeRuntimeMetadataSearchSession", "Closing session")
+ }
}
return AndroidFuture.completedFuture(futureSearchResults)
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 3976ea4fc86e..2220f439f6c8 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -265,7 +265,7 @@ public final class DisplayDeviceConfigTest {
mDisplayDeviceConfig.getPowerThrottlingConfigData();
assertNotNull(powerThrottlingConfigData);
assertEquals(0.1f, powerThrottlingConfigData.brightnessLowestCapAllowed, SMALL_DELTA);
- assertEquals(15f, powerThrottlingConfigData.customAnimationRateSec, SMALL_DELTA);
+ assertEquals(15f, powerThrottlingConfigData.customAnimationRate, SMALL_DELTA);
assertEquals(20000, powerThrottlingConfigData.pollingWindowMaxMillis);
assertEquals(10000, powerThrottlingConfigData.pollingWindowMinMillis);
}
@@ -1299,7 +1299,7 @@ public final class DisplayDeviceConfigTest {
private String getPowerThrottlingConfig() {
return "<powerThrottlingConfig >\n"
+ "<brightnessLowestCapAllowed>0.1</brightnessLowestCapAllowed>\n"
- + "<customAnimationRateSec>15</customAnimationRateSec>\n"
+ + "<customAnimationRate>15</customAnimationRate>\n"
+ "<pollingWindowMaxMillis>20000</pollingWindowMaxMillis>\n"
+ "<pollingWindowMinMillis>10000</pollingWindowMinMillis>\n"
+ "<powerThrottlingMap>\n"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 1a1c8e50ba0a..94eab9cda968 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -25,6 +25,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -1068,9 +1069,9 @@ public class DisplayManagerServiceTest {
firstDisplayId);
}
- /** Tests that the virtual device is created in a device display group. */
+ /** Tests that a trusted virtual display is created in a device display group. */
@Test
- public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+ public void createVirtualDisplay_addsTrustedDisplaysToDeviceDisplayGroups() throws Exception {
DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
DisplayManagerInternal localService = displayManager.new LocalService();
@@ -1081,12 +1082,16 @@ public class DisplayManagerServiceTest {
IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
when(virtualDevice.getDeviceId()).thenReturn(1);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
// Create a first virtual display. A display group should be created for this display on the
// virtual device.
final VirtualDisplayConfig.Builder builder1 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setUniqueId("uniqueId --- device display group 1");
-
+ .setUniqueId("uniqueId --- device display group")
+ .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
int displayId1 =
localService.createVirtualDisplay(
builder1.build(),
@@ -1097,12 +1102,14 @@ public class DisplayManagerServiceTest {
verify(mMockProjectionService, never()).setContentRecordingSession(any(),
nullable(IMediaProjection.class));
int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+ assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP);
// Create a second virtual display. This should be added to the previously created display
// group.
final VirtualDisplayConfig.Builder builder2 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setUniqueId("uniqueId --- device display group 1");
+ .setUniqueId("uniqueId --- device display group")
+ .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
int displayId2 =
localService.createVirtualDisplay(
@@ -1121,6 +1128,36 @@ public class DisplayManagerServiceTest {
displayGroupId2);
}
+ /** Tests that an untrusted virtual display is created in the default display group. */
+ @Test
+ public void createVirtualDisplay_addsUntrustedDisplayToDefaultDisplayGroups() throws Exception {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+
+ registerDefaultDisplays(displayManager);
+ when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+ IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+ when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+ // Create the virtual display. It is untrusted, so it should go into the default group.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setUniqueId("uniqueId --- device display group");
+
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
+ int displayGroupId = localService.getDisplayInfo(displayId).displayGroupId;
+ assertEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP);
+ }
+
/**
* Tests that the virtual display is not added to the device display group when
* VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
@@ -1138,11 +1175,15 @@ public class DisplayManagerServiceTest {
when(virtualDevice.getDeviceId()).thenReturn(1);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+ when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
// Create a first virtual display. A display group should be created for this display on the
// virtual device.
final VirtualDisplayConfig.Builder builder1 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setUniqueId("uniqueId --- device display group");
+ .setUniqueId("uniqueId --- device display group")
+ .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
int displayId1 =
localService.createVirtualDisplay(
@@ -1154,12 +1195,14 @@ public class DisplayManagerServiceTest {
verify(mMockProjectionService, never()).setContentRecordingSession(any(),
nullable(IMediaProjection.class));
int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+ assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP);
// Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
// the display should not be added to the previously created display group.
final VirtualDisplayConfig.Builder builder2 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
+ | VIRTUAL_DISPLAY_FLAG_TRUSTED)
.setUniqueId("uniqueId --- own display group");
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
@@ -1174,6 +1217,7 @@ public class DisplayManagerServiceTest {
verify(mMockProjectionService, never()).setContentRecordingSession(any(),
nullable(IMediaProjection.class));
int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+ assertNotEquals(displayGroupId2, Display.DEFAULT_DISPLAY_GROUP);
assertNotEquals(
"Display 1 should be in the device display group and display 2 in its own display"
@@ -1208,7 +1252,8 @@ public class DisplayManagerServiceTest {
final VirtualDisplayConfig deviceDisplayGroupDisplayConfig =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
.setUniqueId("uniqueId --- device display group 1")
- .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+ | VIRTUAL_DISPLAY_FLAG_TRUSTED)
.build();
int deviceDisplayGroupDisplayId =
@@ -1235,6 +1280,7 @@ public class DisplayManagerServiceTest {
.setUniqueId("uniqueId --- own display group 1")
.setFlags(
VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+ | VIRTUAL_DISPLAY_FLAG_TRUSTED
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
.build();
@@ -1852,7 +1898,7 @@ public class DisplayManagerServiceTest {
/**
* Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when the permission
- * ADD_TRUSTED_DISPLAY is granted.
+ * ADD_TRUSTED_DISPLAY is granted and that display is not in the default display group.
*/
@Test
public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission()
@@ -1881,6 +1927,9 @@ public class DisplayManagerServiceTest {
DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
assertNotNull(ddi);
assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+
+ int displayGroupId = bs.getDisplayInfo(displayId).displayGroupId;
+ assertNotEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP);
}
/**
@@ -1915,11 +1964,11 @@ public class DisplayManagerServiceTest {
}
/**
- * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when called with
- * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted.
+ * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is not allowed when called with
+ * a virtual device, if ADD_TRUSTED_DISPLAY is not granted.
*/
@Test
- public void testOwnDisplayGroup_allowCreationWithVirtualDevice() throws Exception {
+ public void testOwnDisplayGroup_disallowCreationWithVirtualDevice() throws Exception {
DisplayManagerService displayManager =
new DisplayManagerService(mContext, mBasicInjector);
DisplayManagerInternal localService = displayManager.new LocalService();
@@ -1940,16 +1989,16 @@ public class DisplayManagerServiceTest {
when(virtualDevice.getDeviceId()).thenReturn(1);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
- int displayId = localService.createVirtualDisplay(builder.build(),
- mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
- mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
- verify(mMockProjectionService, never()).setContentRecordingSession(any(),
- nullable(IMediaProjection.class));
- performTraversalInternal(displayManager);
- displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
- DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
- assertNotNull(ddi);
- assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+ try {
+ localService.createVirtualDisplay(builder.build(),
+ mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
+ fail("Creating virtual display with VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP without "
+ + "ADD_TRUSTED_DISPLAY permission should throw SecurityException even if "
+ + "called with a virtual device.");
+ } catch (SecurityException e) {
+ // SecurityException is expected
+ }
}
/**
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index f72816889c3b..782262d3f7c9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -18,6 +18,7 @@ package com.android.server.display;
import static android.hardware.display.DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED;
import static android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
import static com.google.common.truth.Truth.assertThat;
@@ -36,6 +37,7 @@ import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.RemoteException;
import android.os.Temperature;
+import android.view.Display;
import android.view.DisplayInfo;
import androidx.test.filters.SmallTest;
@@ -97,6 +99,8 @@ public class ExternalDisplayPolicyTest {
@Mock
private LogicalDisplay mMockedLogicalDisplay;
@Mock
+ private LogicalDisplay mMockedDefaultDisplay;
+ @Mock
private DisplayNotificationManager mMockedDisplayNotificationManager;
@Mock
private ExternalDisplayStatsService mMockedExternalDisplayStatsService;
@@ -141,6 +145,15 @@ public class ExternalDisplayPolicyTest {
when(mMockedLogicalDisplay.getDisplayInfoLocked()).thenReturn(mockedLogicalDisplayInfo);
when(mMockedLogicalDisplayMapper.getDisplayLocked(EXTERNAL_DISPLAY_ID)).thenReturn(
mMockedLogicalDisplay);
+
+ // Initialize default logical display
+ when(mMockedDefaultDisplay.getDisplayIdLocked()).thenReturn(Display.DEFAULT_DISPLAY);
+ when(mMockedDefaultDisplay.isEnabledLocked()).thenReturn(true);
+ final var mockedDefaultDisplayInfo = new DisplayInfo();
+ mockedDefaultDisplayInfo.type = TYPE_INTERNAL;
+ when(mMockedDefaultDisplay.getDisplayInfoLocked()).thenReturn(mockedDefaultDisplayInfo);
+ when(mMockedLogicalDisplayMapper.getDisplayLocked(Display.DEFAULT_DISPLAY)).thenReturn(
+ mMockedDefaultDisplay);
}
@Test
@@ -293,6 +306,52 @@ public class ExternalDisplayPolicyTest {
verify(mMockedLogicalDisplayMapper, never()).forEachLocked(any());
}
+ @Test
+ public void testMirroringAlwaysConfirmedByUser_flagDisabled() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(false);
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)).isTrue();
+ }
+
+ @Test
+ public void testMirroringConfirmed_afterBootForEnabledDisplay() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+ .isTrue();
+ }
+
+ @Test
+ public void testMirroringNotConfirmed_afterBootForDisabledDisplay() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false);
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+ .isFalse();
+ }
+
+ @Test
+ public void testMirroringNeverConfirmed_forNonExternalDisplays() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.DEFAULT_DISPLAY))
+ .isFalse();
+ }
+
+ @Test
+ public void testMirroringNeverConfirmed_forNonExistingDisplays() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.INVALID_DISPLAY))
+ .isFalse();
+ }
+
+ @Test
+ public void testMirroringNeverConfirmed_duringBoot() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+ .isFalse();
+ }
+
private void setTemperature(final IThermalEventListener thermalEventListener,
final List<Temperature> temperature) throws RemoteException {
for (var t : temperature) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 1729ad5ff19f..d831cf8a3643 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -121,6 +121,8 @@ public class LogicalDisplayMapperTest {
Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
private static final DeviceState DEVICE_STATE_OPEN = createDeviceState(2, "Two",
Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
+ private static final DeviceState DEVICE_STATE_EMULATED = createDeviceState(3, "Three",
+ Set.of(DeviceState.PROPERTY_EMULATED_ONLY), Collections.emptySet());
private static final int FLAG_GO_TO_SLEEP_ON_FOLD = 0;
private static final int FLAG_GO_TO_SLEEP_FLAG_SOFT_SLEEP = 2;
private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
@@ -686,6 +688,14 @@ public class LogicalDisplayMapperTest {
}
@Test
+ public void testDeviceShouldNotBeWokenWhenExitingEmulatedState() {
+ assertFalse(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN,
+ DEVICE_STATE_EMULATED,
+ /* isInteractive= */false,
+ /* isBootCompleted= */true));
+ }
+
+ @Test
public void testDeviceShouldBePutToSleep() {
assertTrue(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED,
DEVICE_STATE_OPEN,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
index 5676a388acff..6d14065b6248 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -459,7 +459,6 @@ public class PersistentDataStoreTest {
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
newInjector.setReadStream(bais);
newDataStore.loadIfNeeded();
- assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice));
assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f);
assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
index 1731590be3c9..026e72f117b4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java
@@ -31,12 +31,14 @@ import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
import static android.app.AppOpsManager.UID_STATE_TOP;
+import static android.permission.flags.Flags.delayUidStateChangesFromCapabilityUpdates;
import static com.android.server.appop.AppOpsUidStateTracker.processStateToUidState;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -325,6 +327,10 @@ public class AppOpsUidStateTrackerTest {
.backgroundState()
.update();
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.microphoneCapability()
@@ -342,10 +348,23 @@ public class AppOpsUidStateTrackerTest {
.microphoneCapability()
.update();
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.update();
+ if (delayUidStateChangesFromCapabilityUpdates()) {
+ mClock.advanceTime(mConstants.BG_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED,
+ mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO,
+ MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ }
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
assertEquals(MODE_IGNORED,
mIntf.evalMode(UID, OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, MODE_FOREGROUND));
@@ -357,6 +376,8 @@ public class AppOpsUidStateTrackerTest {
.backgroundState()
.update();
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.cameraCapability()
@@ -372,10 +393,18 @@ public class AppOpsUidStateTrackerTest {
.cameraCapability()
.update();
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.update();
+ if (delayUidStateChangesFromCapabilityUpdates()) {
+ mClock.advanceTime(mConstants.BG_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ }
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
}
@@ -385,6 +414,9 @@ public class AppOpsUidStateTrackerTest {
.backgroundState()
.update();
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.locationCapability()
@@ -401,15 +433,55 @@ public class AppOpsUidStateTrackerTest {
.locationCapability()
.update();
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+
procStateBuilder(UID)
.backgroundState()
.update();
+ if (delayUidStateChangesFromCapabilityUpdates()) {
+ mClock.advanceTime(mConstants.BG_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ }
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_FINE_LOCATION, MODE_FOREGROUND));
}
@Test
+ public void testProcStateChangesAndStaysUnrestrictedAndCapabilityRemoved() {
+ assumeTrue(delayUidStateChangesFromCapabilityUpdates());
+
+ procStateBuilder(UID)
+ .topState()
+ .microphoneCapability()
+ .cameraCapability()
+ .locationCapability()
+ .update();
+
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+
+ procStateBuilder(UID)
+ .foregroundState()
+ .update();
+
+ mClock.advanceTime(mConstants.TOP_STATE_SETTLE_TIME - 1);
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_ALLOWED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+
+ mClock.advanceTime(1);
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_RECORD_AUDIO, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_CAMERA, MODE_FOREGROUND));
+ assertEquals(MODE_IGNORED, mIntf.evalMode(UID, OP_COARSE_LOCATION, MODE_FOREGROUND));
+ }
+
+ @Test
public void testVisibleAppWidget() {
procStateBuilder(UID)
.backgroundState()
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 584fd6270c69..40b9c61a0597 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -25,6 +25,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSess
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX;
+import static com.android.server.job.Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
@@ -303,6 +304,12 @@ public class QuotaControllerTest {
}
}
+ private int getProcessStateQuotaFreeThreshold() {
+ synchronized (mQuotaController.mLock) {
+ return mQuotaController.getProcessStateQuotaFreeThreshold();
+ }
+ }
+
private void setProcessState(int procState) {
setProcessState(procState, mSourceUid);
}
@@ -315,7 +322,7 @@ public class QuotaControllerTest {
final boolean contained = foregroundUids.get(uid);
mUidObserver.onUidStateChanged(uid, procState, 0,
ActivityManager.PROCESS_CAPABILITY_NONE);
- if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ if (procState <= getProcessStateQuotaFreeThreshold()) {
if (!contained) {
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1))
.put(eq(uid), eq(true));
@@ -1371,7 +1378,7 @@ public class QuotaControllerTest {
}
setDischarging();
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
@@ -1473,7 +1480,7 @@ public class QuotaControllerTest {
}
setDischarging();
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -1505,7 +1512,7 @@ public class QuotaControllerTest {
createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS,
timeUsedMs, 5), true);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -4126,7 +4133,7 @@ public class QuotaControllerTest {
}
advanceElapsedClock(5 * SECOND_IN_MILLIS);
// Change to a state that should still be considered foreground.
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
advanceElapsedClock(5 * SECOND_IN_MILLIS);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
@@ -4134,6 +4141,36 @@ public class QuotaControllerTest {
assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
}
+ /** Tests that Timers count FOREGROUND_SERVICE jobs. */
+ @Test
+ @RequiresFlagsEnabled(FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS)
+ public void testTimerTracking_Fgs() {
+ setDischarging();
+
+ JobStatus jobStatus = createJobStatus("testTimerTracking_Fgs", 1);
+ setProcessState(ActivityManager.PROCESS_STATE_BOUND_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ // Change to FOREGROUND_SERVICE state that should count.
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
+ }
+ List<TimingSession> expected = new ArrayList<>();
+ expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
/**
* Tests that Timers properly track sessions when switching between foreground and background
* states.
@@ -4180,7 +4217,7 @@ public class QuotaControllerTest {
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -4213,7 +4250,7 @@ public class QuotaControllerTest {
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -4262,7 +4299,7 @@ public class QuotaControllerTest {
}
assertEquals(0, stats.jobCountInRateLimitingWindow);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg1);
}
@@ -4412,7 +4449,7 @@ public class QuotaControllerTest {
mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
}
advanceElapsedClock(5 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg1);
}
@@ -4625,7 +4662,7 @@ public class QuotaControllerTest {
// App still in foreground so everything should be in quota.
advanceElapsedClock(20 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
@@ -5901,7 +5938,7 @@ public class QuotaControllerTest {
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -5935,7 +5972,7 @@ public class QuotaControllerTest {
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -6056,7 +6093,7 @@ public class QuotaControllerTest {
mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
}
advanceElapsedClock(5 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg1);
}
@@ -6534,7 +6571,7 @@ public class QuotaControllerTest {
// App still in foreground so everything should be in quota.
advanceElapsedClock(20 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
assertTrue(jobTop2.isExpeditedQuotaApproved());
assertTrue(jobFg.isExpeditedQuotaApproved());
assertTrue(jobBg.isExpeditedQuotaApproved());
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 2724149d859f..c645c0852f1b 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -113,6 +113,7 @@
<uses-permission android:name="android.permission.MANAGE_ROLE_HOLDERS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
<queries>
<package android:name="com.android.servicestests.apps.suspendtestapp" />
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
index c76392b30276..5134737ae660 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
@@ -236,6 +236,27 @@ class MouseKeysInterceptorTest {
}
@Test
+ fun whenScrollToggleOn_ScrollRightKeyIsPressed_scrollEventIsSent() {
+ // There should be some delay between the downTime of the key event and calling onKeyEvent
+ val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
+ val keyCodeScrollToggle = MouseKeysInterceptor.MouseKeyEvent.SCROLL_TOGGLE.keyCodeValue
+ val keyCodeScroll = MouseKeysInterceptor.MouseKeyEvent.RIGHT_MOVE_OR_SCROLL.keyCodeValue
+
+ val scrollToggleDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+ keyCodeScrollToggle, 0, 0, DEVICE_ID, 0)
+ val scrollDownEvent = KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+ keyCodeScroll, 0, 0, DEVICE_ID, 0)
+
+ mouseKeysInterceptor.onKeyEvent(scrollToggleDownEvent, 0)
+ mouseKeysInterceptor.onKeyEvent(scrollDownEvent, 0)
+ testLooper.dispatchAll()
+
+ // Verify the sendScrollEvent method is called once and capture the arguments
+ verifyScrollEvents(arrayOf<Float>(-MouseKeysInterceptor.MOUSE_SCROLL_STEP),
+ arrayOf<Float>(0f))
+ }
+
+ @Test
fun whenScrollToggleOff_DirectionalUpKeyIsPressed_RelativeEventIsSent() {
// There should be some delay between the downTime of the key event and calling onKeyEvent
val downTime = clock.now() - KEYBOARD_POST_EVENT_DELAY_MILLIS
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
index 6aa8a32dd7db..06ebe6e28809 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
@@ -62,6 +62,7 @@ import androidx.test.filters.FlakyTest;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
import org.junit.Before;
@@ -92,12 +93,16 @@ public class MagnificationConnectionManagerTest {
private MagnificationConnectionManager.Callback mMockCallback;
private MockContentResolver mResolver;
private MagnificationConnectionManager mMagnificationConnectionManager;
+ @Mock
+ private UserManagerInternal mMockUserManagerInternal;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
LocalServices.addService(StatusBarManagerInternal.class, mMockStatusBarManagerInternal);
+ LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal);
mResolver = new MockContentResolver();
mMockConnection = new MockMagnificationConnection();
mMagnificationConnectionManager = new MagnificationConnectionManager(mContext, new Object(),
@@ -110,6 +115,8 @@ public class MagnificationConnectionManagerTest {
Settings.Secure.putFloatForUser(mResolver,
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.5f,
CURRENT_USER_ID);
+
+ when(mMockUserManagerInternal.isVisibleBackgroundFullUser(anyInt())).thenReturn(false);
}
private void stubSetConnection(boolean needDelay) {
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
index c970a3e34d12..840e5c58078b 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java
@@ -65,7 +65,6 @@ public class AppOpsActiveWatcherTest {
VirtualDeviceRule.withAdditionalPermissions(
Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
- Manifest.permission.CREATE_VIRTUAL_DEVICE,
Manifest.permission.GET_APP_OPS_STATS
);
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java
index 7f2327aa4f24..e3eca6d5fd83 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsDeviceAwareServiceTest.java
@@ -58,7 +58,6 @@ public class AppOpsDeviceAwareServiceTest {
VirtualDeviceRule.withAdditionalPermissions(
Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
- Manifest.permission.CREATE_VIRTUAL_DEVICE,
Manifest.permission.GET_APP_OPS_STATS);
private static final String ATTRIBUTION_TAG_1 = "attributionTag1";
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
index 1abd4eb6157f..b0846f62628c 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java
@@ -22,16 +22,14 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpNotedListener;
import android.companion.virtual.VirtualDeviceManager;
-import android.companion.virtual.VirtualDeviceParams;
import android.content.AttributionSource;
import android.content.Context;
import android.os.Process;
-import android.virtualdevice.cts.common.FakeAssociationRule;
+import android.virtualdevice.cts.common.VirtualDeviceRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -42,8 +40,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
-import java.util.concurrent.atomic.AtomicInteger;
-
/**
* Tests watching noted ops.
*/
@@ -51,7 +47,7 @@ import java.util.concurrent.atomic.AtomicInteger;
@RunWith(AndroidJUnit4.class)
public class AppOpsNotedWatcherTest {
@Rule
- public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
+ public VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault();
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
@Test
@@ -119,19 +115,12 @@ public class AppOpsNotedWatcherTest {
public void testWatchNotedOpsForExternalDevice() {
final AppOpsManager.OnOpNotedListener listener = mock(
AppOpsManager.OnOpNotedListener.class);
- final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
- VirtualDeviceManager.class);
- AtomicInteger virtualDeviceId = new AtomicInteger();
- runWithShellPermissionIdentity(() -> {
- final VirtualDeviceManager.VirtualDevice virtualDevice =
- virtualDeviceManager.createVirtualDevice(
- mFakeAssociationRule.getAssociationInfo().getId(),
- new VirtualDeviceParams.Builder().setName("virtual_device").build());
- virtualDeviceId.set(virtualDevice.getDeviceId());
- });
+ final VirtualDeviceManager.VirtualDevice virtualDevice =
+ mVirtualDeviceRule.createManagedVirtualDevice();
+ final int virtualDeviceId = virtualDevice.getDeviceId();
AttributionSource attributionSource = new AttributionSource(Process.myUid(),
getContext().getOpPackageName(), getContext().getAttributionTag(),
- virtualDeviceId.get());
+ virtualDeviceId);
final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
appOpsManager.startWatchingNoted(new int[]{AppOpsManager.OP_FINE_LOCATION,
@@ -142,7 +131,7 @@ public class AppOpsNotedWatcherTest {
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
eq(Process.myUid()), eq(getContext().getOpPackageName()),
- eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()),
+ eq(getContext().getAttributionTag()), eq(virtualDeviceId),
eq(AppOpsManager.OP_FLAG_SELF), eq(AppOpsManager.MODE_ALLOWED));
appOpsManager.finishOp(getContext().getAttributionSource().getToken(),
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
index 8a6ba4d484f7..d46fb90f40d6 100644
--- a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java
@@ -16,8 +16,6 @@
package com.android.server.appop;
-import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
-
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -28,11 +26,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpStartedListener;
import android.companion.virtual.VirtualDeviceManager;
-import android.companion.virtual.VirtualDeviceParams;
import android.content.AttributionSource;
import android.content.Context;
import android.os.Process;
-import android.virtualdevice.cts.common.FakeAssociationRule;
+import android.virtualdevice.cts.common.VirtualDeviceRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -43,15 +40,13 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
-import java.util.concurrent.atomic.AtomicInteger;
-
/** Tests watching started ops. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class AppOpsStartedWatcherTest {
@Rule
- public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule();
+ public VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault();
private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
@Test
@@ -124,20 +119,13 @@ public class AppOpsStartedWatcherTest {
@Test
public void testWatchStartedOpsForExternalDevice() {
- final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService(
- VirtualDeviceManager.class);
- AtomicInteger virtualDeviceId = new AtomicInteger();
- runWithShellPermissionIdentity(() -> {
- final VirtualDeviceManager.VirtualDevice virtualDevice =
- virtualDeviceManager.createVirtualDevice(
- mFakeAssociationRule.getAssociationInfo().getId(),
- new VirtualDeviceParams.Builder().setName("virtual_device").build());
- virtualDeviceId.set(virtualDevice.getDeviceId());
- });
+ final VirtualDeviceManager.VirtualDevice virtualDevice =
+ mVirtualDeviceRule.createManagedVirtualDevice();
+ final int virtualDeviceId = virtualDevice.getDeviceId();
final OnOpStartedListener listener = mock(OnOpStartedListener.class);
AttributionSource attributionSource = new AttributionSource(Process.myUid(),
getContext().getOpPackageName(), getContext().getAttributionTag(),
- virtualDeviceId.get());
+ virtualDeviceId);
final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
appOpsManager.startWatchingStarted(new int[]{AppOpsManager.OP_FINE_LOCATION,
@@ -150,7 +138,7 @@ public class AppOpsStartedWatcherTest {
verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
.times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION),
eq(Process.myUid()), eq(getContext().getOpPackageName()),
- eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()),
+ eq(getContext().getAttributionTag()), eq(virtualDeviceId),
eq(AppOpsManager.OP_FLAG_SELF),
eq(AppOpsManager.MODE_ALLOWED), eq(OnOpStartedListener.START_TYPE_STARTED),
eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE),
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 62f5edce4e36..e09933a55fb9 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -50,7 +50,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
-import android.Manifest;
import android.app.WindowConfiguration;
import android.app.admin.DevicePolicyManager;
import android.companion.AssociationInfo;
@@ -113,10 +112,11 @@ import android.view.Display;
import android.view.DisplayInfo;
import android.view.KeyEvent;
import android.view.WindowManager;
+import android.virtualdevice.cts.common.VirtualDeviceRule;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.compatibility.common.util.AdoptShellPermissionsRule;
+import com.android.compatibility.common.util.SystemUtil;
import com.android.internal.app.BlockedAppStreamingActivity;
import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
@@ -224,9 +224,7 @@ public class VirtualDeviceManagerServiceTest {
public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Rule
- public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
- InstrumentationRegistry.getInstrumentation().getUiAutomation(),
- Manifest.permission.CREATE_VIRTUAL_DEVICE);
+ public VirtualDeviceRule mVirtualDeviceRule = VirtualDeviceRule.createDefault();
private Context mContext;
private InputManagerMockHelper mInputManagerMockHelper;
@@ -247,6 +245,8 @@ public class VirtualDeviceManagerServiceTest {
@Mock
private IDisplayManager mIDisplayManager;
@Mock
+ private WindowManager mWindowManager;
+ @Mock
private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback;
@Mock
private DevicePolicyManager mDevicePolicyManagerMock;
@@ -385,8 +385,7 @@ public class VirtualDeviceManagerServiceTest {
// Allow virtual devices to be created on the looper thread for testing.
final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
mInputController = new InputController(mNativeWrapperMock,
- new Handler(TestableLooper.get(this).getLooper()),
- mContext.getSystemService(WindowManager.class),
+ new Handler(TestableLooper.get(this).getLooper()), mWindowManager,
AttributionSource.myAttributionSource(), threadVerifier);
mCameraAccessController =
new CameraAccessController(mContext, mLocalService, mCameraAccessBlockedCallback);
@@ -537,7 +536,7 @@ public class VirtualDeviceManagerServiceTest {
.build();
mDeviceImpl.close();
mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
GenericWindowPolicyController gwpc =
mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1);
@@ -545,6 +544,21 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
+ public void getDevicePolicy_customRecentsPolicy_untrustedDisplaygwpcShowsRecentsOnHostDevice() {
+ VirtualDeviceParams params = new VirtualDeviceParams
+ .Builder()
+ .setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM)
+ .build();
+ mDeviceImpl.close();
+ mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+
+ GenericWindowPolicyController gwpc =
+ mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1);
+ assertThat(gwpc.canShowTasksInHostDeviceRecents()).isTrue();
+ }
+
+ @Test
public void getDeviceOwnerUid_oneDevice_returnsCorrectId() {
int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId());
assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid());
@@ -694,7 +708,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void getPreferredLocaleListForApp_keyboardAttached_returnLocaleHints() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), Sets.newArraySet(UID_1));
@@ -734,8 +748,8 @@ public class VirtualDeviceManagerServiceTest {
.setLanguageTag("fr-FR")
.build();
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- addVirtualDisplay(secondDevice, DISPLAY_ID_2);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
+ addVirtualDisplay(secondDevice, DISPLAY_ID_2, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(firstKeyboardConfig, BINDER);
secondDevice.createVirtualKeyboard(secondKeyboardConfig, secondBinder);
@@ -912,11 +926,24 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
- public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException {
+ public void onVirtualDisplayCreatedLocked_notTrustedDisplay_noWakeLockIsAcquired()
+ throws RemoteException {
verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
nullable(String.class), nullable(String.class), nullable(WorkSource.class),
nullable(String.class), anyInt(), eq(null));
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ TestableLooper.get(this).processAllMessages();
+ verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
+ nullable(String.class), nullable(String.class), nullable(WorkSource.class),
+ nullable(String.class), anyInt(), eq(null));
+ }
+
+ @Test
+ public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException {
+ verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
+ nullable(String.class), nullable(String.class), nullable(WorkSource.class),
+ nullable(String.class), anyInt(), eq(null));
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(),
nullable(String.class), nullable(String.class), nullable(WorkSource.class),
nullable(String.class), eq(DISPLAY_ID_1), eq(null));
@@ -925,7 +952,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired()
throws RemoteException {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
assertThrows(IllegalStateException.class,
() -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1));
TestableLooper.get(this).processAllMessages();
@@ -936,7 +963,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void onVirtualDisplayRemovedLocked_wakeLockIsReleased() throws RemoteException {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class);
TestableLooper.get(this).processAllMessages();
verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(),
@@ -951,7 +978,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void addVirtualDisplay_displayNotReleased_wakeLockIsReleased() throws RemoteException {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class);
TestableLooper.get(this).processAllMessages();
verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(),
@@ -972,24 +999,52 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
+ public void createVirtualDpad_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualKeyboard_noDisplay_failsSecurityException() {
assertThrows(SecurityException.class,
() -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
}
@Test
+ public void createVirtualKeyboard_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualMouse_noDisplay_failsSecurityException() {
assertThrows(SecurityException.class,
() -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
}
@Test
+ public void createVirtualMouse_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualTouchscreen_noDisplay_failsSecurityException() {
assertThrows(SecurityException.class,
() -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
}
@Test
+ public void createVirtualTouchscreen_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class,
() -> new VirtualTouchscreenConfig.Builder(
@@ -1005,7 +1060,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
VirtualTouchscreenConfig positiveConfig =
new VirtualTouchscreenConfig.Builder(
/* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800)
@@ -1028,6 +1083,14 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
+ public void createVirtualNavigationTouchpad_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+ BINDER));
+ }
+
+ @Test
public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
assertThrows(IllegalArgumentException.class,
() -> new VirtualNavigationTouchpadConfig.Builder(
@@ -1043,7 +1106,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
VirtualNavigationTouchpadConfig positiveConfig =
new VirtualNavigationTouchpadConfig.Builder(
/* touchpadHeight= */ 50, /* touchpadWidth= */ 50)
@@ -1069,69 +1132,70 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualDpad_noPermission_failsSecurityException() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)));
}
@Test
public void createVirtualKeyboard_noPermission_failsSecurityException() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)));
}
@Test
public void createVirtualMouse_noPermission_failsSecurityException() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)));
}
@Test
public void createVirtualTouchscreen_noPermission_failsSecurityException() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)));
}
@Test
public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
- BINDER));
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualNavigationTouchpad(
+ NAVIGATION_TOUCHPAD_CONFIG,
+ BINDER)));
}
@Test
public void onAudioSessionStarting_noPermission_failsSecurityException() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.onAudioSessionStarting(
- DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.onAudioSessionStarting(
+ DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback)));
}
@Test
public void onAudioSessionEnded_noPermission_failsSecurityException() {
- try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
- assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
- }
+ // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
+ SystemUtil.runWithShellPermissionIdentity(() ->
+ assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded()));
}
@Test
public void createVirtualDpad_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER);
assertWithMessage("Virtual dpad should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1141,7 +1205,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
assertWithMessage("Virtual keyboard should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1151,7 +1215,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualKeyboard_keyboardCreated_localeUpdated() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
assertWithMessage("Virtual keyboard should register fd when the display matches")
.that(mInputController.getInputDeviceDescriptors())
@@ -1172,7 +1236,7 @@ public class VirtualDeviceManagerServiceTest {
.setAssociatedDisplayId(DISPLAY_ID_1)
.build();
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(configWithoutExplicitLayoutInfo, BINDER);
assertWithMessage("Virtual keyboard should register fd when the display matches")
.that(mInputController.getInputDeviceDescriptors())
@@ -1193,7 +1257,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualMouse_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER);
assertWithMessage("Virtual mouse should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1203,7 +1267,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER);
assertWithMessage("Virtual touchscreen should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1213,7 +1277,7 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER);
assertWithMessage("Virtual navigation touchpad should register fd when the display matches")
.that(
@@ -1473,9 +1537,9 @@ public class VirtualDeviceManagerServiceTest {
@Test
public void setShowPointerIcon_setsValueForAllDisplays() {
- addVirtualDisplay(mDeviceImpl, 1);
- addVirtualDisplay(mDeviceImpl, 2);
- addVirtualDisplay(mDeviceImpl, 3);
+ addVirtualDisplay(mDeviceImpl, 1, Display.FLAG_TRUSTED);
+ addVirtualDisplay(mDeviceImpl, 2, Display.FLAG_TRUSTED);
+ addVirtualDisplay(mDeviceImpl, 3, Display.FLAG_TRUSTED);
VirtualMouseConfig config1 = new VirtualMouseConfig.Builder()
.setAssociatedDisplayId(1)
.setInputDeviceName(DEVICE_NAME_1)
@@ -1508,6 +1572,14 @@ public class VirtualDeviceManagerServiceTest {
}
@Test
+ public void setShowPointerIcon_untrustedDisplay_pointerIconIsAlwaysShown() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ clearInvocations(mInputManagerInternalMock);
+ mDeviceImpl.setShowPointerIcon(false);
+ verify(mInputManagerInternalMock, times(0)).setPointerIconVisible(eq(false), anyInt());
+ }
+
+ @Test
public void openNonBlockedAppOnVirtualDisplay_doesNotStartBlockedAlertActivity() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
@@ -1969,15 +2041,20 @@ public class VirtualDeviceManagerServiceTest {
}
private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId) {
+ addVirtualDisplay(virtualDevice, displayId, /* flags= */ 0);
+ }
+
+ private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) {
when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
eq(virtualDevice), any(), any())).thenReturn(displayId);
- virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
final String uniqueId = UNIQUE_ID + displayId;
doAnswer(inv -> {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.uniqueId = uniqueId;
+ displayInfo.flags = flags;
return displayInfo;
}).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+ virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
}
@@ -2002,18 +2079,4 @@ public class VirtualDeviceManagerServiceTest {
/* timeApprovedMs= */0, /* lastTimeConnectedMs= */0,
/* systemDataSyncFlags= */ -1, /* deviceIcon= */ null);
}
-
- /** Helper class to drop permissions temporarily and restore them at the end of a test. */
- static final class DropShellPermissionsTemporarily implements AutoCloseable {
- DropShellPermissionsTemporarily() {
- InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .dropShellPermissionIdentity();
- }
-
- @Override
- public void close() {
- InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .adoptShellPermissionIdentity();
- }
- }
}
diff --git a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
index 9df7a3612a92..1d075401832d 100644
--- a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
+++ b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -33,6 +34,11 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.Build;
+import android.os.Process;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.runner.AndroidJUnit4;
@@ -43,6 +49,7 @@ import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.server.LocalServices;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -55,6 +62,8 @@ import java.util.Set;
public class PlatformCompatTest {
private static final String PACKAGE_NAME = "my.package";
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private Context mContext;
@Mock
@@ -441,4 +450,79 @@ public class PlatformCompatTest {
assertThat(mPlatformCompat.isChangeEnabled(3L, systemAppInfo)).isTrue();
verify(mChangeReporter).reportChange(123, 3L, ChangeReporter.STATE_ENABLED, true, false);
}
+
+ @DisableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK)
+ @Test
+ public void testSharedSystemUidFlagOff() throws Exception {
+ testSharedSystemUid(false);
+ }
+
+ @EnableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK)
+ @Test
+ public void testSharedSystemUidFlagOn() throws Exception {
+ testSharedSystemUid(true);
+ }
+
+ private void testSharedSystemUid(Boolean expectSystemUidTargetSystemSdk) throws Exception {
+ final String systemUidPackageNameTargetsR = "systemuid.package1";
+ final String systemUidPackageNameTargetsQ = "systemuid.package2";
+ final String nonSystemUidPackageNameTargetsR = "nonsystemuid.package1";
+ final String nonSystemUidPackageNameTargetsQ = "nonsystemuid.package2";
+ final int nonSystemUid = 123;
+
+ mCompatConfig =
+ CompatConfigBuilder.create(mBuildClassifier, mContext)
+ .addEnableSinceSdkChangeWithId(Build.VERSION_CODES.R, 1L)
+ .build();
+ mCompatConfig.forceNonDebuggableFinalForTest(true);
+ mPlatformCompat =
+ new PlatformCompat(mContext, mCompatConfig, mBuildClassifier, mChangeReporter);
+
+ ApplicationInfo systemUidAppInfo1 = ApplicationInfoBuilder.create()
+ .withPackageName(systemUidPackageNameTargetsR)
+ .withUid(Process.SYSTEM_UID)
+ .withTargetSdk(Build.VERSION_CODES.R)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(systemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt()))
+ .thenReturn(systemUidAppInfo1);
+
+ ApplicationInfo systemUidAppInfo2 = ApplicationInfoBuilder.create()
+ .withPackageName(systemUidPackageNameTargetsQ)
+ .withUid(Process.SYSTEM_UID)
+ .withTargetSdk(Build.VERSION_CODES.Q)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(systemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt()))
+ .thenReturn(systemUidAppInfo2);
+
+ ApplicationInfo nonSystemUidAppInfo1 = ApplicationInfoBuilder.create()
+ .withPackageName(nonSystemUidPackageNameTargetsR)
+ .withUid(nonSystemUid)
+ .withTargetSdk(Build.VERSION_CODES.R)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(nonSystemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt()))
+ .thenReturn(nonSystemUidAppInfo1);
+
+ ApplicationInfo nonSystemUidAppInfo2 = ApplicationInfoBuilder.create()
+ .withPackageName(nonSystemUidPackageNameTargetsQ)
+ .withUid(nonSystemUid)
+ .withTargetSdk(Build.VERSION_CODES.Q)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(nonSystemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt()))
+ .thenReturn(nonSystemUidAppInfo2);
+
+ when(mPackageManager.getPackagesForUid(eq(Process.SYSTEM_UID)))
+ .thenReturn(new String[] {systemUidPackageNameTargetsR, systemUidPackageNameTargetsQ});
+ when(mPackageManager.getPackagesForUid(eq(nonSystemUid)))
+ .thenReturn(new String[] {
+ nonSystemUidPackageNameTargetsR, nonSystemUidPackageNameTargetsQ
+ });
+
+ assertThat(mPlatformCompat.isChangeEnabledByUid(1L, Process.SYSTEM_UID))
+ .isEqualTo(expectSystemUidTargetSystemSdk);
+ assertThat(mPlatformCompat.isChangeEnabledByUid(1L, nonSystemUid)).isFalse();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 425bb158f997..7e22d74c64e1 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -1256,7 +1256,8 @@ public class MediaProjectionManagerServiceTest {
Manifest.permission.BYPASS_ROLE_QUALIFICATION);
roleManager.setBypassingRoleQualification(true);
- roleManager.addRoleHolderAsUser(role, packageName, /* flags = */ 0, user,
+ roleManager.addRoleHolderAsUser(role, packageName,
+ /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
mContext.getMainExecutor(), success -> {
if (success) {
latch.countDown();
@@ -1271,9 +1272,9 @@ public class MediaProjectionManagerServiceTest {
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
- roleManager.removeRoleHolderAsUser(role, packageName, 0, user,
- mContext.getMainExecutor(), (aBool) -> {
- });
+ roleManager.removeRoleHolderAsUser(role, packageName,
+ /* flags= */ RoleManager.MANAGE_HOLDERS_FLAG_DONT_KILL_APP, user,
+ mContext.getMainExecutor(), (aBool) -> {});
roleManager.setBypassingRoleQualification(false);
instrumentation.getUiAutomation()
.dropShellPermissionIdentity();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 983e694a8f1a..b34b1fb39a7f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -835,7 +835,7 @@ public class NotificationListenersTest extends UiServiceTestCase {
}
@Test
- public void testListenerPost_UpdateLifetimeExtended() throws Exception {
+ public void testListenerPostLifetimeExtended_UpdatesOnlySysui() throws Exception {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
// Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
@@ -856,31 +856,51 @@ public class NotificationListenersTest extends UiServiceTestCase {
Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
.setContentTitle("new title")
.setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
+ .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
nb2.build(), userHandle, null, 0);
NotificationRecord toPost = new NotificationRecord(mContext, sbn2, channel);
// Create system ui-like service.
- ManagedServices.ManagedServiceInfo info = mListeners.new ManagedServiceInfo(
+ ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
- info.isSystemUi = true;
- INotificationListener l1 = mock(INotificationListener.class);
- info.service = l1;
- List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(info);
+ sysuiInfo.isSystemUi = true;
+ INotificationListener sysuiListener = mock(INotificationListener.class);
+ sysuiInfo.service = sysuiListener;
+
+ // Create two non-system ui-like services.
+ ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo1.isSystemUi = false;
+ INotificationListener otherListener1 = mock(INotificationListener.class);
+ otherInfo1.service = otherListener1;
+
+ ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo2.isSystemUi = false;
+ INotificationListener otherListener2 = mock(INotificationListener.class);
+ otherInfo2.service = otherListener2;
+
+ List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
+ otherInfo2);
when(mListeners.getServices()).thenReturn(services);
FieldSetter.setField(mNm,
NotificationManagerService.class.getDeclaredField("mHandler"),
mock(NotificationManagerService.WorkerHandler.class));
doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
- doReturn(mock(NotificationRankingUpdate.class)).when(mNm).makeRankingUpdateLocked(info);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(sysuiInfo);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo1);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo2);
doReturn(false).when(mNm).isInLockDownMode(anyInt());
doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
- // The notification change is posted to the service listener.
+ // Post notification change to the service listeners.
mListeners.notifyPostedLocked(toPost, old);
// Verify that the post occcurs with the updated notification value.
@@ -889,11 +909,190 @@ public class NotificationListenersTest extends UiServiceTestCase {
runnableCaptor.getValue().run();
ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
- verify(l1, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+ verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
StatusBarNotification sbnResult = sbnCaptor.getValue().get();
assertThat(sbnResult.getNotification()
.extras.getCharSequence(Notification.EXTRA_TITLE).toString())
.isEqualTo("new title");
+
+ verify(otherListener1, never()).onNotificationPosted(any(), any());
+ verify(otherListener2, never()).onNotificationPosted(any(), any());
+ }
+
+ @Test
+ public void testListenerPostLifeimteExtension_postsToAppropriateListeners() throws Exception {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+
+ // Create original notification, with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY.
+ String pkg = "pkg";
+ int uid = 9;
+ UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+ NotificationChannel channel = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_HIGH);
+ Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true);
+ StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb.build(), userHandle, null, 0);
+ NotificationRecord leRecord = new NotificationRecord(mContext, sbn, channel);
+
+ // Creates updated notification (without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY)
+ Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("new title")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setFlag(Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false);
+ StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb2.build(), userHandle, null, 0);
+ NotificationRecord nonLeRecord = new NotificationRecord(mContext, sbn2, channel);
+
+ // Create system ui-like service.
+ ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
+ sysuiInfo.isSystemUi = true;
+ INotificationListener sysuiListener = mock(INotificationListener.class);
+ sysuiInfo.service = sysuiListener;
+
+ // Create two non-system ui-like services.
+ ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo1.isSystemUi = false;
+ INotificationListener otherListener1 = mock(INotificationListener.class);
+ otherInfo1.service = otherListener1;
+
+ ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo2.isSystemUi = false;
+ INotificationListener otherListener2 = mock(INotificationListener.class);
+ otherInfo2.service = otherListener2;
+
+ List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
+ otherInfo2);
+ when(mListeners.getServices()).thenReturn(services);
+
+ FieldSetter.setField(mNm,
+ NotificationManagerService.class.getDeclaredField("mHandler"),
+ mock(NotificationManagerService.WorkerHandler.class));
+ doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(sysuiInfo);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo1);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo2);
+ doReturn(false).when(mNm).isInLockDownMode(anyInt());
+ doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
+
+ // The notification change is posted to the service listener.
+ // NonLE to LE should never happen, as LE can't be set in an update by the app.
+ // So we just want to test LE to NonLE.
+ mListeners.notifyPostedLocked(nonLeRecord /*=toPost*/, leRecord /*=old*/);
+
+ // Verify that the post occcurs with the updated notification value.
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mNm.mHandler, times(3)).post(runnableCaptor.capture());
+ List<Runnable> capturedRunnable = runnableCaptor.getAllValues();
+ for (Runnable r : capturedRunnable) {
+ r.run();
+ }
+
+ ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
+ ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
+ verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+ StatusBarNotification sbnResult = sbnCaptor.getValue().get();
+ assertThat(sbnResult.getNotification()
+ .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+ .isEqualTo("new title");
+
+ verify(otherListener1, times(1)).onNotificationPosted(any(), any());
+ verify(otherListener2, times(1)).onNotificationPosted(any(), any());
+ }
+
+ @Test
+ public void testNotifyPostedLocked_postsToAppropriateListeners() throws Exception {
+ // Create original notification
+ String pkg = "pkg";
+ int uid = 9;
+ UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
+ NotificationChannel channel = new NotificationChannel("id", "name",
+ NotificationManager.IMPORTANCE_HIGH);
+ Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb.build(), userHandle, null, 0);
+ NotificationRecord oldRecord = new NotificationRecord(mContext, sbn, channel);
+
+ // Creates updated notification
+ Notification.Builder nb2 = new Notification.Builder(mContext, channel.getId())
+ .setContentTitle("new title")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon);
+ StatusBarNotification sbn2 = new StatusBarNotification(pkg, pkg, 8, "tag", uid, 0,
+ nb2.build(), userHandle, null, 0);
+ NotificationRecord newRecord = new NotificationRecord(mContext, sbn2, channel);
+
+ // Create system ui-like service.
+ ManagedServices.ManagedServiceInfo sysuiInfo = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("a", "a"), sbn2.getUserId(), false, null, 33, 33);
+ sysuiInfo.isSystemUi = true;
+ INotificationListener sysuiListener = mock(INotificationListener.class);
+ sysuiInfo.service = sysuiListener;
+
+ // Create two non-system ui-like services.
+ ManagedServices.ManagedServiceInfo otherInfo1 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("b", "b"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo1.isSystemUi = false;
+ INotificationListener otherListener1 = mock(INotificationListener.class);
+ otherInfo1.service = otherListener1;
+
+ ManagedServices.ManagedServiceInfo otherInfo2 = mListeners.new ManagedServiceInfo(
+ null, new ComponentName("c", "c"), sbn2.getUserId(), false, null, 33, 33);
+ otherInfo2.isSystemUi = false;
+ INotificationListener otherListener2 = mock(INotificationListener.class);
+ otherInfo2.service = otherListener2;
+
+ List<ManagedServices.ManagedServiceInfo> services = ImmutableList.of(otherInfo1, sysuiInfo,
+ otherInfo2);
+ when(mListeners.getServices()).thenReturn(services);
+
+ FieldSetter.setField(mNm,
+ NotificationManagerService.class.getDeclaredField("mHandler"),
+ mock(NotificationManagerService.WorkerHandler.class));
+ doReturn(true).when(mNm).isVisibleToListener(any(), anyInt(), any());
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(sysuiInfo);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo1);
+ doReturn(mock(NotificationRankingUpdate.class)).when(mNm)
+ .makeRankingUpdateLocked(otherInfo2);
+ doReturn(false).when(mNm).isInLockDownMode(anyInt());
+ doNothing().when(mNm).updateUriPermissions(any(), any(), any(), anyInt());
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(sbn2);
+ doReturn(sbn2).when(mListeners).redactStatusBarNotification(any());
+
+ // The notification change is posted to the service listeners.
+ mListeners.notifyPostedLocked(newRecord, oldRecord);
+
+ // Verify that the post occcurs with the updated notification value.
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mNm.mHandler, times(3)).post(runnableCaptor.capture());
+ List<Runnable> capturedRunnable = runnableCaptor.getAllValues();
+ for (Runnable r : capturedRunnable) {
+ r.run();
+ }
+
+ ArgumentCaptor<IStatusBarNotificationHolder> sbnCaptor =
+ ArgumentCaptor.forClass(IStatusBarNotificationHolder.class);
+ verify(sysuiListener, times(1)).onNotificationPosted(sbnCaptor.capture(), any());
+ StatusBarNotification sbnResult = sbnCaptor.getValue().get();
+ assertThat(sbnResult.getNotification()
+ .extras.getCharSequence(Notification.EXTRA_TITLE).toString())
+ .isEqualTo("new title");
+
+ verify(otherListener1, times(1)).onNotificationPosted(any(), any());
+ verify(otherListener2, times(1)).onNotificationPosted(any(), any());
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
index e4436966ae03..c51261f40ed5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
@@ -52,6 +52,7 @@ import android.graphics.Rect;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.view.ContentRecordingSession;
+import android.view.Display;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.SurfaceControl;
@@ -93,9 +94,11 @@ public class ContentRecorderTests extends WindowTestsBase {
private boolean mHandleAnisotropicDisplayMirroring = false;
@Before public void setUp() {
+ mDisplayInfo.type = Display.TYPE_VIRTUAL;
MockitoAnnotations.initMocks(this);
doReturn(INVALID_DISPLAY).when(mWm.mDisplayManagerInternal).getDisplayIdToMirror(anyInt());
+ doReturn(false).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt());
// Skip unnecessary operations of relayout.
spyOn(mWm.mWindowPlacerLocked);
@@ -163,6 +166,25 @@ public class ContentRecorderTests extends WindowTestsBase {
}
@Test
+ public void testUpdateRecording_externalDisplayWithoutUserConfirmation() {
+ mDisplayInfo.type = Display.TYPE_EXTERNAL;
+ defaultInit();
+ mContentRecorder.setContentRecordingSession(mDisplaySession);
+ mContentRecorder.updateRecording();
+ assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
+ }
+
+ @Test
+ public void testUpdateRecording_externalDisplayWithUserConfirmation() {
+ doReturn(true).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt());
+ mDisplayInfo.type = Display.TYPE_EXTERNAL;
+ defaultInit();
+ mContentRecorder.setContentRecordingSession(mDisplaySession);
+ mContentRecorder.updateRecording();
+ assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+ }
+
+ @Test
public void testUpdateRecording_display_invalidDisplayIdToMirror() {
defaultInit();
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
index 5e8f347c0c6e..c8fc4822259e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
@@ -26,7 +26,6 @@ import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertFalse;
@@ -73,7 +72,6 @@ public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBas
when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(0);
when(mMockActivityRecord.findMainWindow()).thenReturn(mMockWindowState);
- spy(mDisplayContent);
doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity();
when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 5b3fd53b197c..7196acc8ec2e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -2933,6 +2933,11 @@ public class TransitionTests extends WindowTestsBase {
controller.requestStartTransition(transit, task, null, null);
player.start();
+ // always include config-at-end activity since it is considered "independent" due to
+ // changing at a different time.
+ assertTrue(player.mLastReady.getChanges().stream()
+ .anyMatch((change -> change.getActivityComponent() != null
+ && (change.getFlags() & TransitionInfo.FLAG_CONFIG_AT_END) != 0)));
assertTrue(activity.isConfigurationDispatchPaused());
player.finish();
assertFalse(activity.isConfigurationDispatchPaused());
@@ -2962,6 +2967,11 @@ public class TransitionTests extends WindowTestsBase {
controller.requestStartTransition(transit, task, null, null);
player.start();
+ // always include config-at-end activity since it is considered "independent" due to
+ // changing at a different time.
+ assertTrue(player.mLastReady.getChanges().stream()
+ .anyMatch((change -> change.getActivityComponent() != null
+ && (change.getFlags() & TransitionInfo.FLAG_CONFIG_AT_END) != 0)));
assertTrue(activity.isConfigurationDispatchPaused());
player.finish();
assertFalse(activity.isConfigurationDispatchPaused());
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 f56825faab73..42e31de295d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.app.ActivityManager.START_CANCELED;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -28,6 +29,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
@@ -37,6 +40,7 @@ import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
@@ -61,6 +65,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.quality.Strictness.LENIENT;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -69,6 +74,7 @@ import android.app.ActivityOptions;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.app.IRequestFinishCallback;
import android.app.PictureInPictureParams;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
@@ -87,6 +93,7 @@ import android.view.WindowInsets;
import android.window.ITaskFragmentOrganizer;
import android.window.ITaskOrganizer;
import android.window.IWindowContainerTransactionCallback;
+import android.window.RemoteTransition;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskAppearedInfo;
@@ -102,6 +109,7 @@ import com.android.window.flags.Flags;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.MockitoSession;
import java.util.ArrayList;
import java.util.HashSet;
@@ -638,6 +646,66 @@ public class WindowOrganizerTests extends WindowTestsBase {
}
@Test
+ public void testStartActivityInTaskFragment_checkCallerPermission() {
+ final ActivityStartController activityStartController =
+ mWm.mAtmService.getActivityStartController();
+ spyOn(activityStartController);
+ final ArgumentCaptor<SafeActivityOptions> activityOptionsCaptor =
+ ArgumentCaptor.forClass(SafeActivityOptions.class);
+
+ final int uid = Binder.getCallingUid();
+ final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build();
+ final WindowContainerTransaction t = new WindowContainerTransaction();
+ final TaskFragmentOrganizer organizer =
+ createTaskFragmentOrganizer(t, true /* isSystemOrganizer */);
+ final IBinder token = new Binder();
+ final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+ .setParentTask(rootTask)
+ .setFragmentToken(token)
+ .setOrganizer(organizer)
+ .createActivityCount(1)
+ .build();
+ mWm.mAtmService.mWindowOrganizerController.mLaunchTaskFragments.put(token, taskFragment);
+ final ActivityRecord ownerActivity = taskFragment.getTopMostActivity();
+
+ // Start Activity in TaskFragment with remote transition.
+ final RemoteTransition transition = mock(RemoteTransition.class);
+ final ActivityOptions options = ActivityOptions.makeRemoteTransition(transition);
+ final Intent intent = new Intent();
+ t.startActivityInTaskFragment(token, ownerActivity.token, intent, options.toBundle());
+ mWm.mAtmService.mWindowOrganizerController.applyTaskFragmentTransactionLocked(
+ t, TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN,
+ false /* shouldApplyIndependently */, null /* remoteTransition */);
+
+ // Get the ActivityOptions.
+ verify(activityStartController).startActivityInTaskFragment(
+ eq(taskFragment), eq(intent), activityOptionsCaptor.capture(),
+ eq(ownerActivity.token), eq(uid), anyInt(), any());
+ final SafeActivityOptions safeActivityOptions = activityOptionsCaptor.getValue();
+
+ final MockitoSession session =
+ mockitoSession().strictness(LENIENT).spyStatic(ActivityTaskManagerService.class)
+ .startMocking();
+ try {
+ // Without the CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS permission, start activity with
+ // remote transition is not allowed.
+ doReturn(PERMISSION_DENIED).when(() -> ActivityTaskManagerService.checkPermission(
+ eq(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS), anyInt(), eq(uid)));
+ assertThrows(SecurityException.class,
+ () -> safeActivityOptions.getOptions(mWm.mAtmService.mTaskSupervisor));
+
+ // With the CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS permission, start activity with
+ // remote transition is allowed.
+ doReturn(PERMISSION_GRANTED).when(() -> ActivityTaskManagerService.checkPermission(
+ eq(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS), anyInt(), eq(uid)));
+ safeActivityOptions.getOptions(mWm.mAtmService.mTaskSupervisor);
+ } finally {
+ session.finishMocking();
+ }
+ }
+
+ @Test
public void testTaskFragmentChangeHidden_throwsWhenNotSystemOrganizer() {
// Non-system organizers are not allow to update the hidden state.
testTaskFragmentChangesWithoutSystemOrganizerThrowException(
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 1a42e80265cb..19a6ddc3a931 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -504,6 +504,11 @@ public class SoundTriggerService extends SystemService {
pw.println("\n##Sound Model Stats dump:\n");
mSoundModelStatTracker.dump(pw);
}
+
+ @Override
+ protected void onUnhandledException(int code, int flags, Exception e) {
+ Slog.wtf(TAG, "Unhandled exception code: " + code, e);
+ }
}
class SoundTriggerSessionStub extends ISoundTriggerSession.Stub {
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 49ca6f34d2d9..44de65a009ff 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1965,13 +1965,14 @@ public final class SatelliteManager {
}
/**
- * Inform whether the device is aligned with the satellite for demo mode.
+ * Inform whether the device is aligned with the satellite in both real and demo mode.
*
- * Framework can send datagram to modem only when device is aligned with the satellite.
- * This method helps framework to simulate the experience of sending datagram over satellite.
+ * In demo mode, framework will send datagram to modem only when device is aligned with
+ * the satellite. This method helps framework to simulate the experience of sending datagram
+ * over satellite.
*
- * @param isAligned {@true} Device is aligned with the satellite for demo mode
- * {@false} Device is not aligned with the satellite for demo mode
+ * @param isAligned {code @true} Device is aligned with the satellite
+ * {code @false} Device is not aligned with the satellite
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 61f01461232f..231c8f551389 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2977,10 +2977,10 @@ interface ITelephony {
void requestTimeForNextSatelliteVisibility(in ResultReceiver receiver);
/**
- * Inform whether the device is aligned with the satellite within in margin for demo mode.
+ * Inform whether the device is aligned with the satellite in both real and demo mode.
*
- * @param isAligned {@true} Device is aligned with the satellite for demo mode
- * {@false} Device is not aligned with the satellite for demo mode
+ * @param isAligned {@true} Device is aligned with the satellite.
+ * {@false} Device is not aligned with the satellite.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
index 634b6eedd7e6..8d27c1d1dfd1 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt
@@ -33,9 +33,9 @@ class LetterboxAppHelper
@JvmOverloads
constructor(
instr: Instrumentation,
- launcherName: String = ActivityOptions.NonResizeablePortraitActivity.LABEL,
+ launcherName: String = ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.LABEL,
component: ComponentNameMatcher =
- ActivityOptions.NonResizeablePortraitActivity.COMPONENT.toFlickerComponent()
+ ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.COMPONENT.toFlickerComponent()
) : StandardAppHelper(instr, launcherName, component) {
private val gestureHelper: GestureHelper = GestureHelper(instrumentation)
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index f891606f0066..f2e34257ef01 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -115,6 +115,19 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity android:name=".NonResizeableFixedAspectRatioPortraitActivity"
+ android:theme="@style/CutoutNever"
+ android:resizeableActivity="false"
+ android:screenOrientation="portrait"
+ android:minAspectRatio="1.77"
+ android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableFixedAspectRatioPortraitActivity"
+ android:label="NonResizeableFixedAspectRatioPortraitActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
<activity android:name=".StartMediaProjectionActivity"
android:theme="@style/CutoutNever"
android:resizeableActivity="false"
@@ -143,6 +156,7 @@
<activity android:name=".LaunchTransparentActivity"
android:resizeableActivity="false"
android:screenOrientation="portrait"
+ android:minAspectRatio="1.77"
android:theme="@style/OptOutEdgeToEdge"
android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity"
android:label="LaunchTransparentActivity"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index e4de2c574553..73625da9dfa5 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -85,6 +85,12 @@ public class ActivityOptions {
FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity");
}
+ public static class NonResizeableFixedAspectRatioPortraitActivity {
+ public static final String LABEL = "NonResizeableFixedAspectRatioPortraitActivity";
+ public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+ FLICKER_APP_PACKAGE + ".NonResizeableFixedAspectRatioPortraitActivity");
+ }
+
public static class StartMediaProjectionActivity {
public static final String LABEL = "StartMediaProjectionActivity";
public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java
new file mode 100644
index 000000000000..be38c259d00d
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java
@@ -0,0 +1,28 @@
+/*
+ * 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.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class NonResizeableFixedAspectRatioPortraitActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.activity_non_resizeable);
+ }
+}
diff --git a/tests/testables/Android.bp b/tests/testables/Android.bp
index f2111856c666..17cc0b2a5884 100644
--- a/tests/testables/Android.bp
+++ b/tests/testables/Android.bp
@@ -35,4 +35,8 @@ java_library {
"androidx.test.rules",
"mockito-target-inline-minus-junit4",
],
+ static_libs: [
+ "PlatformMotionTesting",
+ "kotlinx_coroutines_test",
+ ],
}
diff --git a/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt
new file mode 100644
index 000000000000..b27b8269575b
--- /dev/null
+++ b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation
+
+import android.animation.AnimatorTestRuleToolkit.Companion.TAG
+import android.util.Log
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import platform.test.motion.MotionTestRule
+import platform.test.motion.RecordedMotion
+import platform.test.motion.RecordedMotion.Companion.create
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.Feature
+import platform.test.motion.golden.FrameId
+import platform.test.motion.golden.TimeSeries
+import platform.test.motion.golden.TimeSeriesCaptureScope
+import platform.test.motion.golden.TimestampFrameId
+
+class AnimatorTestRuleToolkit(val animatorTestRule: AnimatorTestRule, val testScope: TestScope) {
+ internal companion object {
+ const val TAG = "AnimatorRuleToolkit"
+ }
+}
+
+/**
+ * Controls the timing of the motion recording.
+ *
+ * The time series is recorded while the [recording] function is running.
+ */
+class MotionControl(val recording: MotionControlFn)
+
+typealias MotionControlFn = suspend MotionControlScope.() -> Unit
+
+interface MotionControlScope {
+ /** Waits until [check] returns true. Invoked on each frame. */
+ suspend fun awaitCondition(check: () -> Boolean)
+
+ /** Waits for [count] frames to be processed. */
+ suspend fun awaitFrames(count: Int = 1)
+}
+
+/** Defines the sampling of features during a test run. */
+data class AnimatorRuleRecordingSpec<T>(
+ /** The root `observing` object, available in [timeSeriesCapture]'s [TimeSeriesCaptureScope]. */
+ val captureRoot: T,
+
+ /** The timing for the recording. */
+ val motionControl: MotionControl,
+
+ /** Time interval between frame captures, in milliseconds. */
+ val frameDurationMs: Long = 16L,
+
+ /** Produces the time-series, invoked on each animation frame. */
+ val timeSeriesCapture: TimeSeriesCaptureScope<T>.() -> Unit,
+)
+
+/** Records the time-series of the features specified in [recordingSpec]. */
+fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion(
+ recordingSpec: AnimatorRuleRecordingSpec<T>,
+): RecordedMotion {
+ with(toolkit.animatorTestRule) {
+ val frameIdCollector = mutableListOf<FrameId>()
+ val propertyCollector = mutableMapOf<String, MutableList<DataPoint<*>>>()
+
+ fun recordFrame(frameId: FrameId) {
+ Log.i(TAG, "recordFrame($frameId)")
+ frameIdCollector.add(frameId)
+ recordingSpec.timeSeriesCapture.invoke(
+ TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector)
+ )
+ }
+
+ val motionControl =
+ MotionControlImpl(
+ toolkit.animatorTestRule,
+ toolkit.testScope,
+ recordingSpec.frameDurationMs,
+ recordingSpec.motionControl,
+ )
+
+ Log.i(TAG, "recordMotion() begin recording")
+
+ val startFrameTime = currentTime
+ while (!motionControl.recordingEnded) {
+ recordFrame(TimestampFrameId(currentTime - startFrameTime))
+ motionControl.nextFrame()
+ }
+
+ Log.i(TAG, "recordMotion() end recording")
+
+ val timeSeries =
+ TimeSeries(
+ frameIdCollector.toList(),
+ propertyCollector.entries.map { entry -> Feature(entry.key, entry.value) },
+ )
+
+ return create(timeSeries, null)
+ }
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+private class MotionControlImpl(
+ val animatorTestRule: AnimatorTestRule,
+ val testScope: TestScope,
+ val frameMs: Long,
+ motionControl: MotionControl,
+) : MotionControlScope {
+ private val recordingJob = motionControl.recording.launch()
+
+ private val frameEmitter = MutableStateFlow<Long>(0)
+ private val onFrame = frameEmitter.asStateFlow()
+
+ var recordingEnded: Boolean = false
+
+ fun nextFrame() {
+ animatorTestRule.advanceTimeBy(frameMs)
+
+ frameEmitter.tryEmit(animatorTestRule.currentTime)
+ testScope.runCurrent()
+
+ if (recordingJob.isCompleted) {
+ recordingEnded = true
+ }
+ }
+
+ override suspend fun awaitCondition(check: () -> Boolean) {
+ onFrame.takeWhile { !check() }.collect {}
+ }
+
+ override suspend fun awaitFrames(count: Int) {
+ onFrame.take(count).collect {}
+ }
+
+ private fun MotionControlFn.launch(): Job {
+ val function = this
+ return testScope.launch { function() }
+ }
+}
diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp
index c23f41a6c3d4..71105646a3d3 100644
--- a/tests/testables/tests/Android.bp
+++ b/tests/testables/tests/Android.bp
@@ -29,13 +29,17 @@ android_test {
"src/**/*.kt",
"src/**/I*.aidl",
],
+ asset_dirs: ["goldens"],
resource_dirs: ["res"],
static_libs: [
+ "PlatformMotionTesting",
"androidx.core_core-animation",
"androidx.core_core-ktx",
+ "androidx.test.ext.junit",
"androidx.test.rules",
"androidx.test.ext.junit",
"hamcrest-library",
+ "kotlinx_coroutines_test",
"mockito-target-inline-minus-junit4",
"testables",
"truth",
diff --git a/tests/testables/tests/goldens/recordMotion_withAnimator.json b/tests/testables/tests/goldens/recordMotion_withAnimator.json
new file mode 100644
index 000000000000..87fece5395e7
--- /dev/null
+++ b/tests/testables/tests/goldens/recordMotion_withAnimator.json
@@ -0,0 +1,64 @@
+{
+ "frame_ids": [
+ 0,
+ 20,
+ 40,
+ 60,
+ 80,
+ 100,
+ 120,
+ 140,
+ 160,
+ 180,
+ 200,
+ 220,
+ 240,
+ 260,
+ 280,
+ 300,
+ 320,
+ 340,
+ 360,
+ 380,
+ 400,
+ 420,
+ 440,
+ 460,
+ 480,
+ 500
+ ],
+ "features": [
+ {
+ "name": "value",
+ "type": "float",
+ "data_points": [
+ 1,
+ 0.9960574,
+ 0.98429155,
+ 0.9648882,
+ 0.9381534,
+ 0.9045085,
+ 0.8644843,
+ 0.818712,
+ 0.76791346,
+ 0.7128896,
+ 0.65450853,
+ 0.5936906,
+ 0.5313952,
+ 0.46860474,
+ 0.40630943,
+ 0.34549147,
+ 0.2871104,
+ 0.23208654,
+ 0.181288,
+ 0.13551569,
+ 0.09549153,
+ 0.061846733,
+ 0.035111785,
+ 0.015708387,
+ 0.003942609,
+ 0
+ ]
+ }
+ ]
+}
diff --git a/tests/testables/tests/goldens/recordMotion_withSpring.json b/tests/testables/tests/goldens/recordMotion_withSpring.json
new file mode 100644
index 000000000000..e9fb5b4af869
--- /dev/null
+++ b/tests/testables/tests/goldens/recordMotion_withSpring.json
@@ -0,0 +1,48 @@
+{
+ "frame_ids": [
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272
+ ],
+ "features": [
+ {
+ "name": "value",
+ "type": "float",
+ "data_points": [
+ 1,
+ 0.9488604,
+ 0.83574325,
+ 0.7016156,
+ 0.5691678,
+ 0.4497436,
+ 0.34789434,
+ 0.26431116,
+ 0.19766562,
+ 0.14572789,
+ 0.10601636,
+ 0.076149896,
+ 0.05401709,
+ 0.037837274,
+ 0.026161024,
+ 0.017839976,
+ 0.011983856,
+ 0.007914998
+ ]
+ }
+ ]
+}
diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt
new file mode 100644
index 000000000000..fbef4899bca9
--- /dev/null
+++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.animation
+
+import android.util.FloatProperty
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.internal.dynamicanimation.animation.SpringAnimation
+import com.android.internal.dynamicanimation.animation.SpringForce
+import kotlinx.coroutines.test.TestScope
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.motion.MotionTestRule
+import platform.test.motion.RecordedMotion
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.asDataPoint
+import platform.test.motion.testing.createGoldenPathManager
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AnimatorTestRuleToolkitTest {
+ companion object {
+ private val GOLDEN_PATH_MANAGER =
+ createGoldenPathManager("frameworks/base/tests/testables/tests/goldens")
+
+ private val TEST_PROPERTY =
+ object : FloatProperty<TestState>("value") {
+ override fun get(state: TestState): Float {
+ return state.animatedValue
+ }
+
+ override fun setValue(state: TestState, value: Float) {
+ state.animatedValue = value
+ }
+ }
+ }
+
+ @get:Rule(order = 0) val animatorTestRule = AnimatorTestRule(this)
+ @get:Rule(order = 1)
+ val motionRule =
+ MotionTestRule(AnimatorTestRuleToolkit(animatorTestRule, TestScope()), GOLDEN_PATH_MANAGER)
+
+ @Test
+ fun recordMotion_withAnimator() {
+ val state = TestState()
+ AnimatorSet().apply {
+ duration = 500
+ play(
+ ValueAnimator.ofFloat(state.animatedValue, 0f).apply {
+ addUpdateListener { state.animatedValue = it.animatedValue as Float }
+ }
+ )
+ getInstrumentation().runOnMainSync { start() }
+ }
+
+ val recordedMotion =
+ record(state, MotionControl { awaitFrames(count = 26) }, sampleIntervalMs = 20L)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordMotion_withAnimator")
+ }
+
+ @Test
+ fun recordMotion_withSpring() {
+ val state = TestState()
+ var isDone = false
+ SpringAnimation(state, TEST_PROPERTY).apply {
+ spring =
+ SpringForce(0f).apply {
+ stiffness = 500f
+ dampingRatio = 0.95f
+ }
+
+ setStartValue(1f)
+ setMinValue(0f)
+ setMaxValue(1f)
+ minimumVisibleChange = 0.01f
+
+ addEndListener { _, _, _, _ -> isDone = true }
+ getInstrumentation().runOnMainSync { start() }
+ }
+
+ val recordedMotion =
+ record(state, MotionControl { awaitCondition { isDone } }, sampleIntervalMs = 16L)
+
+ motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordMotion_withSpring")
+ }
+
+ private fun record(
+ state: TestState,
+ motionControl: MotionControl,
+ sampleIntervalMs: Long,
+ ): RecordedMotion {
+ var recordedMotion: RecordedMotion? = null
+ getInstrumentation().runOnMainSync {
+ recordedMotion =
+ motionRule.recordMotion(
+ AnimatorRuleRecordingSpec(
+ state,
+ motionControl,
+ sampleIntervalMs,
+ ) {
+ feature(
+ FeatureCapture("value") { state -> state.animatedValue.asDataPoint() },
+ "value",
+ )
+ }
+ )
+ }
+ return recordedMotion!!
+ }
+
+ data class TestState(var animatedValue: Float = 1f)
+}