summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp8
-rw-r--r--Android.bp40
-rw-r--r--core/api/current.txt41
-rw-r--r--core/api/test-current.txt7
-rw-r--r--core/api/test-lint-baseline.txt6
-rw-r--r--core/java/android/app/Activity.java8
-rw-r--r--core/java/android/app/IUserSwitchObserver.aidl12
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java47
-rw-r--r--core/java/android/app/UiModeManager.java4
-rw-r--r--core/java/android/app/UserSwitchObserver.java6
-rw-r--r--core/java/android/app/admin/flags/flags.aconfig10
-rw-r--r--core/java/android/app/assist/AssistContent.java71
-rw-r--r--core/java/android/app/jank/AppJankStats.java17
-rw-r--r--core/java/android/companion/virtual/flags.aconfig8
-rw-r--r--core/java/android/content/pm/RegisteredServicesCache.java69
-rw-r--r--core/java/android/content/pm/flags.aconfig8
-rw-r--r--core/java/android/hardware/SystemSensorManager.java43
-rw-r--r--core/java/android/hardware/location/ContextHubManager.java2
-rw-r--r--core/java/android/os/CpuHeadroomParams.java225
-rw-r--r--core/java/android/os/GpuHeadroomParams.java172
-rw-r--r--core/java/android/os/IHintManager.aidl3
-rw-r--r--core/java/android/os/health/SystemHealthManager.java194
-rw-r--r--core/java/android/provider/Settings.java43
-rw-r--r--core/java/android/util/FeatureFlagUtils.java9
-rw-r--r--core/java/android/util/SystemPropertySetter.java103
-rw-r--r--core/java/android/view/WindowInsetsController.java3
-rw-r--r--core/java/android/view/WindowManagerGlobal.java4
-rw-r--r--core/java/android/view/contentprotection/flags/content_protection_flags.aconfig7
-rw-r--r--core/java/android/widget/flags/flags.aconfig2
-rw-r--r--core/java/android/window/flags/windowing_frontend.aconfig2
-rw-r--r--core/java/com/android/internal/jank/Cuj.java19
-rw-r--r--core/java/com/android/internal/policy/PhoneWindow.java37
-rw-r--r--core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java13
-rw-r--r--core/jni/android_util_AssetManager.cpp2
-rw-r--r--core/proto/android/providers/settings/secure.proto10
-rw-r--r--core/res/AndroidManifest.xml7
-rw-r--r--core/res/res/layout/notification_2025_expand_button.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_base.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_call.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_media.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_messaging.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_conversation.xml1
-rw-r--r--core/res/res/layout/notification_2025_template_header.xml1
-rw-r--r--core/res/res/xml/sms_short_codes.xml52
-rw-r--r--core/tests/coretests/Android.bp1
-rw-r--r--core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java5
-rw-r--r--core/tests/coretests/src/android/security/advancedprotection/OWNERS1
-rw-r--r--core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java20
-rw-r--r--core/tests/systemproperties/Android.bp1
-rw-r--r--core/tests/utiltests/Android.bp1
-rw-r--r--libs/WindowManager/Shell/multivalentTests/Android.bp1
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java182
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java13
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt122
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt44
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt202
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java29
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java9
-rw-r--r--media/java/android/media/MediaCodec.java28
-rw-r--r--media/java/android/media/MediaMuxer.java6
-rw-r--r--native/android/libandroid.map.txt3
-rw-r--r--native/android/performance_hint.cpp2
-rw-r--r--native/android/system_health.cpp137
-rw-r--r--native/android/tests/performance_hint/PerformanceHintNativeTest.cpp12
-rw-r--r--packages/CredentialManager/tests/robotests/Android.bp1
-rw-r--r--packages/CredentialManager/wear/robotests/Android.bp1
-rw-r--r--packages/InputDevices/res/raw/keyboard_layout_romanian.kcm357
-rw-r--r--packages/InputDevices/res/values/strings.xml3
-rw-r--r--packages/InputDevices/res/xml/keyboard_layouts.xml7
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java3
-rw-r--r--packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java31
-rw-r--r--packages/SettingsLib/DataStore/tests/Android.bp1
-rw-r--r--packages/SettingsLib/Ipc/Android.bp2
-rw-r--r--packages/SettingsLib/Spa/screenshot/robotests/Android.bp1
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt83
-rw-r--r--packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt178
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java17
-rw-r--r--packages/SettingsLib/tests/robotests/Android.bp5
-rw-r--r--packages/SettingsLib/tests/robotests/fragment/Android.bp4
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java36
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java5
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java5
-rw-r--r--packages/SystemUI/Android.bp6
-rw-r--r--packages/SystemUI/AndroidManifest.xml3
-rw-r--r--packages/SystemUI/aconfig/predictive_back.aconfig21
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java15
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java8
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java88
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt6
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt50
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt11
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt70
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt72
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt16
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt6
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt32
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt96
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt282
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt73
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt119
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt7
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt41
-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.kt198
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt46
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt184
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt124
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt85
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt57
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java92
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt78
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt51
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt82
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt40
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt116
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt)76
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt)5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt)3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt91
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt73
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java71
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt114
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt (renamed from packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt)45
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt (renamed from packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt)10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt (renamed from packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt)2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt (renamed from packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt)2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt53
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt103
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt114
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt63
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt)0
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt18
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt)2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt (renamed from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt)4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt2
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt32
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt208
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt278
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt201
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt37
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt49
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt350
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt83
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt178
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt157
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt110
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt35
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt258
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt103
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt88
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt358
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt538
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt111
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt99
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt61
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt18
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt68
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt13
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt50
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt6
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt24
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt13
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt19
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt7
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt12
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt7
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt2
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt108
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt33
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt100
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt125
-rw-r--r--packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt774
-rw-r--r--packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt69
-rw-r--r--ravenwood/tests/bivalenttest/Android.bp1
-rw-r--r--ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt31
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AutoclickController.java8
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java44
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java6
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java16
-rw-r--r--services/core/java/com/android/server/am/ServiceRecord.java9
-rw-r--r--services/core/java/com/android/server/am/UserController.java173
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java3
-rw-r--r--services/core/java/com/android/server/compat/CompatConfig.java21
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java14
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java28
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java38
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java28
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java7
-rw-r--r--services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java2
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java71
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubService.java19
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java17
-rw-r--r--services/core/java/com/android/server/om/OverlayActorEnforcer.java8
-rw-r--r--services/core/java/com/android/server/power/hint/HintManagerService.java104
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryStatsImpl.java113
-rw-r--r--services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java15
-rw-r--r--services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java15
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java5
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java5
-rw-r--r--services/java/com/android/server/SystemServer.java8
-rw-r--r--services/java/com/android/server/flags.aconfig7
-rw-r--r--services/robotests/Android.bp1
-rw-r--r--services/robotests/backup/Android.bp1
-rw-r--r--services/tests/InputMethodSystemServerTests/Android.bp1
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java1
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java5
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java5
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java86
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java5
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java6
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java6
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java79
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java15
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java4
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java8
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java34
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java10
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java28
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java4
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java16
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java7
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java257
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java41
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java105
-rw-r--r--test-mock/Android.bp1
-rw-r--r--tests/AppJankTest/src/android/app/jank/tests/JankUtils.java1
-rw-r--r--tests/InputScreenshotTest/robotests/Android.bp1
-rw-r--r--tests/Internal/Android.bp1
-rw-r--r--tests/testables/tests/AndroidTest.xml1
318 files changed, 8158 insertions, 4882 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 69892f9ba103..b1c091bfa946 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -107,6 +107,7 @@ aconfig_declarations_group {
"com.android.server.flags.services-aconfig-java",
"com.android.text.flags-aconfig-java",
"com.android.window.flags.window-aconfig-java",
+ "configinfra_framework_flags_java_lib",
"conscrypt_exported_aconfig_flags_lib",
"device_policy_aconfig_flags_lib",
"display_flags_lib",
@@ -1584,6 +1585,13 @@ java_aconfig_library {
}
java_aconfig_library {
+ name: "android.app.appfunctions.flags-aconfig-java-host",
+ aconfig_declarations: "android.app.appfunctions.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ host_supported: true,
+}
+
+java_aconfig_library {
name: "android.app.appfunctions.exported-flags-aconfig-java",
aconfig_declarations: "android.app.appfunctions.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/Android.bp b/Android.bp
index a1f6e3079804..9d8c8a69fc18 100644
--- a/Android.bp
+++ b/Android.bp
@@ -408,7 +408,6 @@ java_defaults {
"bouncycastle-repackaged-unbundled",
"com.android.sysprop.foldlockbehavior",
"com.android.sysprop.view",
- "configinfra_framework_flags_java_lib",
"framework-internal-utils",
"dynamic_instrumentation_manager_aidl-java",
// If MimeMap ever becomes its own APEX, then this dependency would need to be removed
@@ -537,45 +536,6 @@ java_library {
}),
}
-// This is identical to "framework-minus-apex" but with "jarjar_shards" hardcodd.
-// (also "stem" is commented out to avoid a conflict with the "framework-minus-apex")
-// TODO(b/383559945) This module is just for local testing / verification. It's not used
-// by anything. Remove it once we roll out RELEASE_USE_SHARDED_JARJAR_ON_FRAMEWORK_MINUS_APEX.
-java_library {
- name: "framework-minus-apex_jarjar-sharded",
- defaults: [
- "framework-minus-apex-with-libs-defaults",
- "framework-non-updatable-lint-defaults",
- ],
- installable: true,
- // For backwards compatibility.
- // stem: "framework",
- apex_available: ["//apex_available:platform"],
- visibility: [
- "//frameworks/base",
- "//frameworks/base/location",
- // TODO(b/147128803) remove the below lines
- "//frameworks/base/apex/blobstore/framework",
- "//frameworks/base/apex/jobscheduler/framework",
- "//frameworks/base/packages/Tethering/tests/unit",
- "//packages/modules/Connectivity/Tethering/tests/unit",
- ],
- errorprone: {
- javacflags: [
- "-Xep:AndroidFrameworkCompatChange:ERROR",
- "-Xep:AndroidFrameworkUid:ERROR",
- ],
- },
- lint: {
- baseline_filename: "lint-baseline.xml",
- warning_checks: [
- "FlaggedApi",
- ],
- },
- jarjar_prefix: "com.android.internal.hidden_from_bootclasspath",
- jarjar_shards: "10",
-}
-
java_library {
name: "framework-minus-apex-intdefs",
defaults: ["framework-minus-apex-with-libs-defaults"],
diff --git a/core/api/current.txt b/core/api/current.txt
index 59eb31ad9a24..c4109392d6bd 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8947,18 +8947,19 @@ package android.app.assist {
method public android.content.ClipData getClipData();
method public android.os.Bundle getExtras();
method public android.content.Intent getIntent();
+ method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") @Nullable public android.net.Uri getSessionTransferUri();
method public String getStructuredData();
method public android.net.Uri getWebUri();
method public boolean isAppProvidedIntent();
method public boolean isAppProvidedWebUri();
method public void setClipData(android.content.ClipData);
method public void setIntent(android.content.Intent);
+ method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public void setSessionTransferUri(@Nullable android.net.Uri);
method public void setStructuredData(String);
method public void setWebUri(android.net.Uri);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.assist.AssistContent> CREATOR;
field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA";
- field @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public static final String EXTRA_SESSION_TRANSFER_WEB_URI = "android.app.assist.extra.SESSION_TRANSFER_WEB_URI";
}
public class AssistStructure implements android.os.Parcelable {
@@ -9190,8 +9191,9 @@ package android.app.blob {
package android.app.jank {
@FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats {
- ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram);
+ ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram);
method public long getJankyFrameCount();
+ method @Nullable public String getNavigationComponent();
method @NonNull public android.app.jank.RelativeFrameTimeHistogram getRelativeFrameTimeHistogram();
method public long getTotalFrameCount();
method public int getUid();
@@ -33657,16 +33659,23 @@ package android.os {
}
@FlaggedApi("android.os.cpu_gpu_headrooms") public final class CpuHeadroomParams {
- ctor public CpuHeadroomParams();
method public int getCalculationType();
- method @IntRange(from=0x32, to=0x2710) public long getCalculationWindowMillis();
- method public void setCalculationType(int);
- method public void setCalculationWindowMillis(@IntRange(from=0x32, to=0x2710) int);
- method public void setTids(@NonNull int...);
+ method public long getCalculationWindowMillis();
+ method @NonNull public int[] getTids();
+ method @NonNull public android.os.CpuHeadroomParams.Builder toBuilder();
field public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
field public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
}
+ public static final class CpuHeadroomParams.Builder {
+ ctor public CpuHeadroomParams.Builder();
+ ctor public CpuHeadroomParams.Builder(@NonNull android.os.CpuHeadroomParams);
+ method @NonNull public android.os.CpuHeadroomParams build();
+ method @NonNull public android.os.CpuHeadroomParams.Builder setCalculationType(int);
+ method @NonNull public android.os.CpuHeadroomParams.Builder setCalculationWindowMillis(@IntRange(from=1) int);
+ method @NonNull public android.os.CpuHeadroomParams.Builder setTids(@NonNull int...);
+ }
+
public final class CpuUsageInfo implements android.os.Parcelable {
method public int describeContents();
method public long getActive();
@@ -33915,13 +33924,20 @@ package android.os {
}
@FlaggedApi("android.os.cpu_gpu_headrooms") public final class GpuHeadroomParams {
- ctor public GpuHeadroomParams();
method public int getCalculationType();
- method @IntRange(from=0x32, to=0x2710) public int getCalculationWindowMillis();
- method public void setCalculationType(int);
- method public void setCalculationWindowMillis(@IntRange(from=0x32, to=0x2710) int);
+ method public int getCalculationWindowMillis();
field public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
field public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
+ field public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000; // 0x2710
+ field public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50; // 0x32
+ }
+
+ public static final class GpuHeadroomParams.Builder {
+ ctor public GpuHeadroomParams.Builder();
+ ctor public GpuHeadroomParams.Builder(@NonNull android.os.GpuHeadroomParams);
+ method @NonNull public android.os.GpuHeadroomParams build();
+ method @NonNull public android.os.GpuHeadroomParams.Builder setCalculationType(int);
+ method @NonNull public android.os.GpuHeadroomParams.Builder setCalculationWindowMillis(@IntRange(from=1) int);
}
public class Handler {
@@ -35187,9 +35203,12 @@ package android.os.health {
public class SystemHealthManager {
method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getCpuHeadroom(@Nullable android.os.CpuHeadroomParams);
+ method @FlaggedApi("android.os.cpu_gpu_headrooms") @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getCpuHeadroomCalculationWindowRange();
method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getCpuHeadroomMinIntervalMillis();
method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getGpuHeadroom(@Nullable android.os.GpuHeadroomParams);
+ method @FlaggedApi("android.os.cpu_gpu_headrooms") @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getGpuHeadroomCalculationWindowRange();
method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getGpuHeadroomMinIntervalMillis();
+ method @FlaggedApi("android.os.cpu_gpu_headrooms") @IntRange(from=1) public int getMaxCpuHeadroomTidsSize();
method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>);
method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
method public android.os.health.HealthStats takeMyUidSnapshot();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ed8042d45243..cd2cc07b8cc3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2,7 +2,7 @@
package android {
public static final class Manifest.permission {
- field @FlaggedApi("com.android.server.accessibility.motion_event_observing") public static final String ACCESSIBILITY_MOTION_EVENT_OBSERVING = "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING";
+ field public static final String ACCESSIBILITY_MOTION_EVENT_OBSERVING = "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING";
field public static final String ACCESS_NOTIFICATIONS = "android.permission.ACCESS_NOTIFICATIONS";
field public static final String ACTIVITY_EMBEDDING = "android.permission.ACTIVITY_EMBEDDING";
field public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY";
@@ -2113,6 +2113,11 @@ package android.media {
method public boolean isAidlHal();
}
+ public static final class MediaMuxer.OutputFormat {
+ field public static final int MUXER_OUTPUT_FIRST = 0; // 0x0
+ field public static final int MUXER_OUTPUT_LAST = 4; // 0x4
+ }
+
public final class MediaRoute2Info implements android.os.Parcelable {
method @NonNull public String getOriginalId();
}
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 349b4edffc32..fe23517f77ba 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -1977,6 +1977,8 @@ Todo: android.window.WindowContainerTransaction#setActivityWindowingMode(android
Documentation mentions 'TODO'
+UnflaggedApi: android.Manifest.permission#ACCESSIBILITY_MOTION_EVENT_OBSERVING:
+ New API must be flagged with @FlaggedApi: field android.Manifest.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING
UnflaggedApi: android.Manifest.permission#MANAGE_REMOTE_AUTH:
New API must be flagged with @FlaggedApi: field android.Manifest.permission.MANAGE_REMOTE_AUTH
UnflaggedApi: android.Manifest.permission#RESERVED_FOR_TESTING_SIGNATURE:
@@ -2057,6 +2059,10 @@ UnflaggedApi: android.media.AudioManager#getFocusFadeOutDurationForTest():
New API must be flagged with @FlaggedApi: method android.media.AudioManager.getFocusFadeOutDurationForTest()
UnflaggedApi: android.media.AudioManager#getFocusUnmuteDelayAfterFadeOutForTest():
New API must be flagged with @FlaggedApi: method android.media.AudioManager.getFocusUnmuteDelayAfterFadeOutForTest()
+UnflaggedApi: android.media.MediaMuxer.OutputFormat#MUXER_OUTPUT_FIRST:
+ New API must be flagged with @FlaggedApi: field android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_FIRST
+UnflaggedApi: android.media.MediaMuxer.OutputFormat#MUXER_OUTPUT_LAST:
+ New API must be flagged with @FlaggedApi: field android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_LAST
UnflaggedApi: android.media.RingtoneSelection:
New API must be flagged with @FlaggedApi: class android.media.RingtoneSelection
UnflaggedApi: android.media.RingtoneSelection#DEFAULT_SELECTION_URI_STRING:
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 8614bde775ad..b198811416cd 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1273,8 +1273,8 @@ public class Activity extends ContextThemeWrapper
* Requests to show the “Open in browser” education. “Open in browser” is a feature
* within the app header that allows users to switch from an app to the web. The feature
* is made available when an application is opened by a user clicking a link or when a
- * link is provided by an application. Links can be provided by utilizing
- * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+ * link is provided by an application. Links can be provided by calling
+ * {@link AssistContent#setSessionTransferUri} or
* {@link AssistContent#setWebUri}.
*
* <p>This method should be utilized when an activity wants to nudge the user to switch
@@ -1287,7 +1287,7 @@ public class Activity extends ContextThemeWrapper
* disruptive to the user to show the education and when it is optimal to switch the user to a
* browser session. Before requesting to show the education, developers should assert that they
* have set a link that can be used by the "Open in browser" feature through either
- * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+ * {@link AssistContent#setSessionTransferUri} or
* {@link AssistContent#setWebUri} so that users are navigated to a relevant page if they choose
* to switch to the browser. If a URI is not set using either method, "Open in browser" will
* utilize a generic link if available which will direct users to the homepage of the site
@@ -1296,7 +1296,7 @@ public class Activity extends ContextThemeWrapper
* the user will not be provided with the option to switch to the browser and the education will
* not be shown if requested.
*
- * @see android.app.assist.AssistContent#EXTRA_SESSION_TRANSFER_WEB_URI
+ * @see android.app.assist.AssistContent#setSessionTransferUri
*/
@FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
public final void requestOpenInBrowserEducation() {
diff --git a/core/java/android/app/IUserSwitchObserver.aidl b/core/java/android/app/IUserSwitchObserver.aidl
index 1ff7a17e578f..d71ee7c712e7 100644
--- a/core/java/android/app/IUserSwitchObserver.aidl
+++ b/core/java/android/app/IUserSwitchObserver.aidl
@@ -19,10 +19,10 @@ package android.app;
import android.os.IRemoteCallback;
/** {@hide} */
-interface IUserSwitchObserver {
- void onBeforeUserSwitching(int newUserId);
- oneway void onUserSwitching(int newUserId, IRemoteCallback reply);
- oneway void onUserSwitchComplete(int newUserId);
- oneway void onForegroundProfileSwitch(int newProfileId);
- oneway void onLockedBootComplete(int newUserId);
+oneway interface IUserSwitchObserver {
+ void onBeforeUserSwitching(int newUserId, IRemoteCallback reply);
+ void onUserSwitching(int newUserId, IRemoteCallback reply);
+ void onUserSwitchComplete(int newUserId);
+ void onForegroundProfileSwitch(int newProfileId);
+ void onLockedBootComplete(int newUserId);
}
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 5567c085d205..e7e9b0027812 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -36,6 +36,7 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.util.SystemPropertySetter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -157,21 +158,6 @@ public class PropertyInvalidatedCache<Query, Result> {
public static final String MODULE_TELEPHONY = "telephony";
/**
- * Constants that affect retries when the process is unable to write the property.
- * The first constant is the number of times the process will attempt to set the
- * property. The second constant is the delay between attempts.
- */
-
- /**
- * Wait 200ms between retry attempts and the retry limit is 5. That gives a total possible
- * delay of 1s, which should be less than ANR timeouts. The goal is to have the system crash
- * because the property could not be set (which is a condition that is easily recognized) and
- * not crash because of an ANR (which can be confusing to debug).
- */
- private static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
- private static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
-
- /**
* Construct a system property that matches the rules described above. The module is
* one of the permitted values above. The API is a string that is a legal Java simple
* identifier. The api is modified to conform to the system property style guide by
@@ -958,37 +944,8 @@ public class PropertyInvalidatedCache<Query, Result> {
*/
@Override
void setNonceInternal(long value) {
- // Failing to set the nonce is a fatal error. Failures setting a system property have
- // been reported; given that the failure is probably transient, this function includes
- // a retry.
final String str = Long.toString(value);
- RuntimeException failure = null;
- for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
- try {
- SystemProperties.set(mName, str);
- if (attempt > 0) {
- // This log is not guarded. Based on known bug reports, it should
- // occur once a week or less. The purpose of the log message is to
- // identify the retries as a source of delay that might be otherwise
- // be attributed to the cache itself.
- Log.w(TAG, "Nonce set after " + attempt + " tries");
- }
- return;
- } catch (RuntimeException e) {
- if (failure == null) {
- failure = e;
- }
- try {
- Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
- } catch (InterruptedException x) {
- // Ignore this exception. The desired delay is only approximate and
- // there is no issue if the sleep sometimes terminates early.
- }
- }
- }
- // This point is reached only if SystemProperties.set() fails at least once.
- // Rethrow the first exception that was received.
- throw failure;
+ SystemPropertySetter.setWithRetry(mName, str);
}
}
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 2e6f3e1c7f0a..57549847f05d 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -753,7 +753,7 @@ public class UiModeManager {
* <p>
* The mode can be one of:
* <ul>
- * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+ * <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into
* {@code notnight} mode</li>
* <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
* {@code night} mode</li>
@@ -889,7 +889,7 @@ public class UiModeManager {
* <p>
* The mode can be one of:
* <ul>
- * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+ * <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into
* {@code notnight} mode</li>
* <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
* {@code night} mode</li>
diff --git a/core/java/android/app/UserSwitchObserver.java b/core/java/android/app/UserSwitchObserver.java
index 727799a1f948..1664cfb6f7a8 100644
--- a/core/java/android/app/UserSwitchObserver.java
+++ b/core/java/android/app/UserSwitchObserver.java
@@ -30,7 +30,11 @@ public class UserSwitchObserver extends IUserSwitchObserver.Stub {
}
@Override
- public void onBeforeUserSwitching(int newUserId) throws RemoteException {}
+ public void onBeforeUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
+ if (reply != null) {
+ reply.sendResult(null);
+ }
+ }
@Override
public void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index af035cb630dc..75589fa47892 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -160,16 +160,6 @@ flag {
}
flag {
- name: "fix_race_condition_in_tie_profile_lock"
- namespace: "enterprise"
- description: "Fix race condition in tieProfileLockIfNecessary()"
- bug: "355905501"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "quiet_mode_credential_bug_fix"
namespace: "enterprise"
description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped."
diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java
index 3e3ca2488bd3..adf8c94ff8d5 100644
--- a/core/java/android/app/assist/AssistContent.java
+++ b/core/java/android/app/assist/AssistContent.java
@@ -1,6 +1,7 @@
package android.app.assist;
import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ClipData;
import android.content.Intent;
@@ -30,31 +31,6 @@ public class AssistContent implements Parcelable {
public static final String EXTRA_APP_FUNCTION_DATA =
"android.app.assist.extra.APP_FUNCTION_DATA";
- /**
- * This extra can be optionally supplied in the {@link #getExtras} bundle to provide a
- * {@link Uri} which will be utilized when transitioning a user's session to another surface.
- *
- * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the
- * "Open in browser" feature will use this URI to transition the current session from one
- * surface to the other. Apps may choose to encode session or user information into this
- * URI in order to provide a better session transfer experience.
- *
- * <p>Unlike {@link #setWebUri}, this URI will not be used for features where the user might
- * accidentally share it with another user. However, developers should not encode
- * authentication credentials into this URI, because it will be surfaced in the browser URL
- * bar and may be copied and shared from there.
- *
- * <p>When providing this extra, developers should still continue to provide
- * {@link #setWebUri} for backwards compatibility with features such as
- * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing">
- * recents URL sharing</a> which do not benefit from a session-transfer web URI.
- *
- * @see android.app.Activity#requestOpenInBrowserEducation()
- */
- @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
- public static final String EXTRA_SESSION_TRANSFER_WEB_URI =
- "android.app.assist.extra.SESSION_TRANSFER_WEB_URI";
-
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private boolean mIsAppProvidedIntent = false;
private boolean mIsAppProvidedWebUri = false;
@@ -66,6 +42,7 @@ public class AssistContent implements Parcelable {
private ClipData mClipData;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private Uri mUri;
+ private Uri mSessionTransferUri;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private final Bundle mExtras;
@@ -200,6 +177,41 @@ public class AssistContent implements Parcelable {
}
/**
+ * This method can be used to provide a {@link Uri} which will be utilized when transitioning a
+ * user's session to another surface.
+ *
+ * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the
+ * "Open in browser" feature will use this URI to transition the current session from one
+ * surface to the other. Apps may choose to encode session or user information into this
+ * URI in order to provide a better session transfer experience. However, while this URI will
+ * only be available to the system and not other applications, developers should not encode
+ * authentication credentials into this URI, because it will be surfaced in the browser URL bar
+ * and may be copied and shared from there.
+ *
+ * <p>When providing this URI, developers should still continue to provide
+ * {@link #setWebUri} for backwards compatibility with features such as
+ * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing">
+ * recents URL sharing</a> which facilitate link sharing with other users and would not benefit
+ * from a session-transfer URI.
+ *
+ * @see android.app.Activity#requestOpenInBrowserEducation()
+ */
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
+ public void setSessionTransferUri(@Nullable Uri uri) {
+ mSessionTransferUri = uri;
+ }
+
+ /**
+ * Return the content's session transfer web URI as per
+ * {@link #setSessionTransferUri(android.net.Uri)}, or null if there is none.
+ */
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
+ @Nullable
+ public Uri getSessionTransferUri() {
+ return mSessionTransferUri;
+ }
+
+ /**
* Return Bundle for extra vendor-specific data that can be modified and examined.
*/
public Bundle getExtras() {
@@ -218,6 +230,9 @@ public class AssistContent implements Parcelable {
mUri = Uri.CREATOR.createFromParcel(in);
}
if (in.readInt() != 0) {
+ mSessionTransferUri = Uri.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
mStructuredData = in.readString();
}
mIsAppProvidedIntent = in.readInt() == 1;
@@ -245,6 +260,12 @@ public class AssistContent implements Parcelable {
} else {
dest.writeInt(0);
}
+ if (mSessionTransferUri != null) {
+ dest.writeInt(1);
+ mSessionTransferUri.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
if (mStructuredData != null) {
dest.writeInt(1);
dest.writeString(mStructuredData);
diff --git a/core/java/android/app/jank/AppJankStats.java b/core/java/android/app/jank/AppJankStats.java
index 6ef6a44ddfbb..a8ebc383b7b5 100644
--- a/core/java/android/app/jank/AppJankStats.java
+++ b/core/java/android/app/jank/AppJankStats.java
@@ -57,6 +57,8 @@ public final class AppJankStats {
// Histogram of relative frame times encoded in predetermined buckets.
private RelativeFrameTimeHistogram mRelativeFrameTimeHistogram;
+ // Navigation component associated to this stat.
+ private String mNavigationComponent;
/** Used to indicate no widget category has been set. */
public static final String WIDGET_CATEGORY_UNSPECIFIED = "unspecified";
@@ -158,6 +160,8 @@ public final class AppJankStats {
*
* @param appUid the Uid of the App that is collecting jank stats.
* @param widgetId the widget id that frames will be associated to.
+ * @param navigationComponent the intended navigation target within the activity, this could be
+ * a navigation destination, screen and/or pane.
* @param widgetCategory a category used to organize widgets in a structured way that indicates
* they serve a similar purpose or perform related functions. Must be
* prefixed with WIDGET_CATEGORY_ and have a suffix of one of the
@@ -172,14 +176,14 @@ public final class AppJankStats {
* @param jankyFrames the total number of janky frames that were counted for this stat.
* @param relativeFrameTimeHistogram the histogram with predefined buckets. See
* {@link #getRelativeFrameTimeHistogram()} for details.
- *
*/
- public AppJankStats(int appUid, @NonNull String widgetId,
+ public AppJankStats(int appUid, @NonNull String widgetId, @Nullable String navigationComponent,
@Nullable @WidgetCategory String widgetCategory,
@Nullable @WidgetState String widgetState, long totalFrames, long jankyFrames,
@NonNull RelativeFrameTimeHistogram relativeFrameTimeHistogram) {
mUid = appUid;
mWidgetId = widgetId;
+ mNavigationComponent = navigationComponent;
mWidgetCategory = widgetCategory != null ? widgetCategory : WIDGET_CATEGORY_UNSPECIFIED;
mWidgetState = widgetState != null ? widgetState : WIDGET_STATE_UNSPECIFIED;
mTotalFrames = totalFrames;
@@ -254,4 +258,13 @@ public final class AppJankStats {
public @NonNull RelativeFrameTimeHistogram getRelativeFrameTimeHistogram() {
return mRelativeFrameTimeHistogram;
}
+
+ /**
+ * Returns the navigation component if it exists that this stat applies to.
+ *
+ * @return the navigation component if it exists that this stat applies to.
+ */
+ public @Nullable String getNavigationComponent() {
+ return mNavigationComponent;
+ }
}
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 46da4a3d99bc..f31e7d4c61cd 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -11,14 +11,6 @@ package: "android.companion.virtual.flags"
container: "system"
flag {
- name: "enable_native_vdm"
- namespace: "virtual_devices"
- description: "Enable native VDM service"
- bug: "303535376"
- is_fixed_read_only: true
-}
-
-flag {
name: "dynamic_policy"
is_exported: true
namespace: "virtual_devices"
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 0333942b7f3e..9d11710a2cad 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -17,6 +17,7 @@
package android.content.pm;
import android.Manifest;
+import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -30,6 +31,7 @@ import android.os.Environment;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.AttributeSet;
import android.util.IntArray;
@@ -45,11 +47,11 @@ import com.android.internal.util.ArrayUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
-import libcore.io.IoUtils;
-
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
+import libcore.io.IoUtils;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -94,6 +96,9 @@ public abstract class RegisteredServicesCache<V> {
@GuardedBy("mServicesLock")
private final SparseArray<UserServices<V>> mUserServices = new SparseArray<UserServices<V>>(2);
+ @GuardedBy("mServicesLock")
+ private final ArrayMap<String, ServiceInfo<V>> mServiceInfoCaches = new ArrayMap<>();
+
private static class UserServices<V> {
@GuardedBy("mServicesLock")
final Map<V, Integer> persistentServices = Maps.newHashMap();
@@ -323,13 +328,16 @@ public abstract class RegisteredServicesCache<V> {
public final ComponentName componentName;
@UnsupportedAppUsage
public final int uid;
+ public final long lastUpdateTime;
/** @hide */
- public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName) {
+ public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName,
+ long lastUpdateTime) {
this.type = type;
this.componentInfo = componentInfo;
this.componentName = componentName;
this.uid = (componentInfo != null) ? componentInfo.applicationInfo.uid : -1;
+ this.lastUpdateTime = lastUpdateTime;
}
@Override
@@ -490,7 +498,7 @@ public abstract class RegisteredServicesCache<V> {
final List<ResolveInfo> resolveInfos = queryIntentServices(userId);
for (ResolveInfo resolveInfo : resolveInfos) {
try {
- ServiceInfo<V> info = parseServiceInfo(resolveInfo);
+ ServiceInfo<V> info = parseServiceInfo(resolveInfo, userId);
if (info == null) {
Log.w(TAG, "Unable to load service info " + resolveInfo.toString());
continue;
@@ -638,13 +646,31 @@ public abstract class RegisteredServicesCache<V> {
}
@VisibleForTesting
- protected ServiceInfo<V> parseServiceInfo(ResolveInfo service)
+ protected ServiceInfo<V> parseServiceInfo(ResolveInfo service, int userId)
throws XmlPullParserException, IOException {
android.content.pm.ServiceInfo si = service.serviceInfo;
ComponentName componentName = new ComponentName(si.packageName, si.name);
PackageManager pm = mContext.getPackageManager();
+ // Check if the service has been in the service cache.
+ long lastUpdateTime = -1;
+ if (Flags.optimizeParsingInRegisteredServicesCache()) {
+ try {
+ PackageInfo packageInfo = pm.getPackageInfoAsUser(si.packageName,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
+ lastUpdateTime = packageInfo.lastUpdateTime;
+
+ ServiceInfo<V> serviceInfo = getServiceInfoFromServiceCache(si, lastUpdateTime);
+ if (serviceInfo != null) {
+ return serviceInfo;
+ }
+ } catch (NameNotFoundException | SecurityException e) {
+ Slog.d(TAG, "Fail to get the PackageInfo in parseServiceInfo: " + e);
+ }
+ }
+
XmlResourceParser parser = null;
try {
parser = si.loadXmlMetaData(pm, mMetaDataName);
@@ -670,8 +696,13 @@ public abstract class RegisteredServicesCache<V> {
if (v == null) {
return null;
}
- final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo;
- return new ServiceInfo<V>(v, serviceInfo, componentName);
+ ServiceInfo<V> serviceInfo = new ServiceInfo<V>(v, si, componentName, lastUpdateTime);
+ if (Flags.optimizeParsingInRegisteredServicesCache()) {
+ synchronized (mServicesLock) {
+ mServiceInfoCaches.put(getServiceCacheKey(si), serviceInfo);
+ }
+ }
+ return serviceInfo;
} catch (NameNotFoundException e) {
throw new XmlPullParserException(
"Unable to load resources for pacakge " + si.packageName);
@@ -841,4 +872,28 @@ public abstract class RegisteredServicesCache<V> {
mContext.unregisterReceiver(mExternalReceiver);
mContext.unregisterReceiver(mUserRemovedReceiver);
}
+
+ private static String getServiceCacheKey(@NonNull android.content.pm.ServiceInfo serviceInfo) {
+ StringBuilder sb = new StringBuilder(serviceInfo.packageName);
+ sb.append('-');
+ sb.append(serviceInfo.name);
+ return sb.toString();
+ }
+
+ private ServiceInfo<V> getServiceInfoFromServiceCache(
+ @NonNull android.content.pm.ServiceInfo serviceInfo, long lastUpdateTime) {
+ String serviceCacheKey = getServiceCacheKey(serviceInfo);
+ synchronized (mServicesLock) {
+ ServiceInfo<V> serviceCache = mServiceInfoCaches.get(serviceCacheKey);
+ if (serviceCache == null) {
+ return null;
+ }
+ if (serviceCache.lastUpdateTime == lastUpdateTime) {
+ return serviceCache;
+ }
+ // The service is not latest, remove it from the cache.
+ mServiceInfoCaches.remove(serviceCacheKey);
+ return null;
+ }
+ }
}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 7bba06c87813..e4b8c90d381d 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -383,3 +383,11 @@ flag {
bug: "334024639"
description: "Feature flag to check whether a given UID can access a content provider"
}
+
+flag {
+ name: "optimize_parsing_in_registered_services_cache"
+ namespace: "package_manager_service"
+ description: "Feature flag to optimize RegisteredServicesCache ServiceInfo parsing by using caches."
+ bug: "319137634"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java
index 2d3d25217357..868429c30631 100644
--- a/core/java/android/hardware/SystemSensorManager.java
+++ b/core/java/android/hardware/SystemSensorManager.java
@@ -16,8 +16,6 @@
package android.hardware;
-import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED;
-import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
import static android.content.Context.DEVICE_ID_DEFAULT;
@@ -164,11 +162,7 @@ public class SystemSensorManager extends SensorManager {
// initialize the sensor list
for (int index = 0;; ++index) {
Sensor sensor = new Sensor();
- if (android.companion.virtual.flags.Flags.enableNativeVdm()) {
- if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
- } else {
- if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break;
- }
+ if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
mFullSensorsList.add(sensor);
mHandleToSensor.put(sensor.getHandle(), sensor);
}
@@ -555,11 +549,7 @@ public class SystemSensorManager extends SensorManager {
}
private List<Sensor> createRuntimeSensorListLocked(int deviceId) {
- if (android.companion.virtual.flags.Flags.vdmPublicApis()) {
- setupVirtualDeviceListener();
- } else {
- setupRuntimeSensorBroadcastReceiver();
- }
+ setupVirtualDeviceListener();
List<Sensor> list = new ArrayList<>();
nativeGetRuntimeSensors(mNativeInstance, deviceId, list);
mFullRuntimeSensorListByDevice.put(deviceId, list);
@@ -570,35 +560,6 @@ public class SystemSensorManager extends SensorManager {
return list;
}
- private void setupRuntimeSensorBroadcastReceiver() {
- if (mRuntimeSensorBroadcastReceiver == null) {
- mRuntimeSensorBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) {
- synchronized (mFullRuntimeSensorListByDevice) {
- final int deviceId = intent.getIntExtra(
- EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_DEFAULT);
- List<Sensor> removedSensors =
- mFullRuntimeSensorListByDevice.removeReturnOld(deviceId);
- if (removedSensors != null) {
- for (Sensor s : removedSensors) {
- cleanupSensorConnection(s);
- }
- }
- mRuntimeSensorListByDeviceByType.remove(deviceId);
- }
- }
- }
- };
-
- IntentFilter filter = new IntentFilter("virtual_device_removed");
- filter.addAction(ACTION_VIRTUAL_DEVICE_REMOVED);
- mContext.registerReceiver(mRuntimeSensorBroadcastReceiver, filter,
- Context.RECEIVER_NOT_EXPORTED);
- }
- }
-
private void setupVirtualDeviceListener() {
if (mVirtualDeviceListener != null) {
return;
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 0cd320981c93..9181bd0cb2ed 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -697,6 +697,7 @@ public final class ContextHubManager {
*
* @param endpointId Statically generated ID for an endpoint.
* @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -733,6 +734,7 @@ public final class ContextHubManager {
* cannot be null or empty.
* @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
* @throws IllegalArgumentException if the serviceDescriptor is empty/null.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
diff --git a/core/java/android/os/CpuHeadroomParams.java b/core/java/android/os/CpuHeadroomParams.java
index 072c012d6db9..e51197607520 100644
--- a/core/java/android/os/CpuHeadroomParams.java
+++ b/core/java/android/os/CpuHeadroomParams.java
@@ -28,15 +28,11 @@ import java.util.Arrays;
/**
* Headroom request params used by {@link SystemHealthManager#getCpuHeadroom(CpuHeadroomParams)}.
+ *
+ * <p>This class is immutable and one should use the {@link Builder} to build a new instance.
*/
@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
public final class CpuHeadroomParams {
- final CpuHeadroomParamsInternal mInternal;
-
- public CpuHeadroomParams() {
- mInternal = new CpuHeadroomParamsInternal();
- }
-
/** @hide */
@IntDef(flag = false, prefix = {"CPU_HEADROOM_CALCULATION_TYPE_"}, value = {
CPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
@@ -47,107 +43,188 @@ public final class CpuHeadroomParams {
}
/**
- * Calculates the headroom based on minimum value over a device-defined window.
+ * The headroom calculation type bases on minimum value over a specified window.
*/
public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
/**
- * Calculates the headroom based on average value over a device-defined window.
+ * The headroom calculation type bases on average value over a specified window.
*/
public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
- private static final int CALCULATION_WINDOW_MILLIS_MIN = 50;
- private static final int CALCULATION_WINDOW_MILLIS_MAX = 10000;
- private static final int MAX_TID_COUNT = 5;
+ /** @hide */
+ public final CpuHeadroomParamsInternal mInternal;
+
+ private CpuHeadroomParams() {
+ mInternal = new CpuHeadroomParamsInternal();
+ }
+
+ public static final class Builder {
+ private int mCalculationType = -1;
+ private int mCalculationWindowMillis = -1;
+ private int[] mTids = null;
+
+ public Builder() {
+ }
+
+ /**
+ * Returns a new builder copy with the same values as the params.
+ */
+ public Builder(@NonNull CpuHeadroomParams params) {
+ if (params.mInternal.calculationType >= 0) {
+ mCalculationType = params.mInternal.calculationType;
+ }
+ if (params.mInternal.calculationWindowMillis >= 0) {
+ mCalculationWindowMillis = params.mInternal.calculationWindowMillis;
+ }
+ if (params.mInternal.tids != null) {
+ mTids = Arrays.copyOf(params.mInternal.tids, params.mInternal.tids.length);
+ }
+ }
+
+ /**
+ * Sets the headroom calculation type.
+ * <p>
+ *
+ * @throws IllegalArgumentException if the type is invalid.
+ */
+ @NonNull
+ public Builder setCalculationType(
+ @CpuHeadroomCalculationType int calculationType) {
+ switch (calculationType) {
+ case CPU_HEADROOM_CALCULATION_TYPE_MIN:
+ case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE: {
+ mCalculationType = calculationType;
+ return this;
+ }
+ }
+ throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+ }
+
+ /**
+ * Sets the headroom calculation window size in milliseconds.
+ * <p>
+ *
+ * @param windowMillis the window size in milliseconds ranges from
+ * {@link SystemHealthManager#getCpuHeadroomCalculationWindowRange()}.
+ * The smaller the window size, the larger fluctuation in the headroom
+ * value should be expected. The default value can be retrieved from
+ * the {@link CpuHeadroomParams#getCalculationWindowMillis}. The device
+ * will try to use the closest feasible window size to this param.
+ * @throws IllegalArgumentException if the window is invalid.
+ */
+ @NonNull
+ public Builder setCalculationWindowMillis(@IntRange(from = 1) int windowMillis) {
+ if (windowMillis <= 0) {
+ throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
+ }
+ mCalculationWindowMillis = windowMillis;
+ return this;
+ }
+
+ /**
+ * Sets the thread TIDs to track.
+ * <p>
+ * The TIDs should belong to the same of the process that will make the headroom call. And
+ * they should not have different core affinity.
+ * <p>
+ * If not set or set to empty, the headroom will be based on the PID of the process making
+ * the call.
+ *
+ * @param tids non-null list of TIDs, where maximum size can be read from
+ * {@link SystemHealthManager#getMaxCpuHeadroomTidsSize()}.
+ * @throws IllegalArgumentException if the TID is not positive.
+ */
+ @NonNull
+ public Builder setTids(@NonNull int... tids) {
+ for (int tid : tids) {
+ if (tid <= 0) {
+ throw new IllegalArgumentException("Invalid TID: " + tid);
+ }
+ }
+ mTids = tids;
+ return this;
+ }
+
+ /**
+ * Builds the {@link CpuHeadroomParams} object.
+ */
+ @NonNull
+ public CpuHeadroomParams build() {
+ CpuHeadroomParams params = new CpuHeadroomParams();
+ if (mCalculationType >= 0) {
+ params.mInternal.calculationType = (byte) mCalculationType;
+ }
+ if (mCalculationWindowMillis >= 0) {
+ params.mInternal.calculationWindowMillis = mCalculationWindowMillis;
+ }
+ if (mTids != null) {
+ params.mInternal.tids = mTids;
+ }
+ return params;
+ }
+ }
/**
- * Sets the headroom calculation type.
- * <p>
- *
- * @throws IllegalArgumentException if the type is invalid.
+ * Returns a new builder with the same values as this object.
*/
- public void setCalculationType(@CpuHeadroomCalculationType int calculationType) {
- switch (calculationType) {
- case CPU_HEADROOM_CALCULATION_TYPE_MIN:
- case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
- mInternal.calculationType = (byte) calculationType;
- return;
- }
- throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+ @NonNull
+ public Builder toBuilder() {
+ return new Builder(this);
}
/**
* Gets the headroom calculation type.
- * Default to {@link #CPU_HEADROOM_CALCULATION_TYPE_MIN} if not set.
+ * <p>
+ * This will return the default value chosen by the device if not set.
*/
public @CpuHeadroomCalculationType int getCalculationType() {
@CpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
- case CPU_HEADROOM_CALCULATION_TYPE_MIN, CPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
- mInternal.calculationType;
+ case CPU_HEADROOM_CALCULATION_TYPE_MIN,
+ CPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> mInternal.calculationType;
default -> CPU_HEADROOM_CALCULATION_TYPE_MIN;
};
return validatedType;
}
/**
- * Sets the headroom calculation window size in milliseconds.
- * <p>
- *
- * @param windowMillis the window size in milliseconds ranges from [50, 10000]. The smaller the
- * window size, the larger fluctuation in the headroom value should be
- * expected. The default value can be retrieved from the
- * {@link #getCalculationWindowMillis}. The device will try to use the
- * closest feasible window size to this param.
- * @throws IllegalArgumentException if the window size is not in allowed range.
- */
- public void setCalculationWindowMillis(
- @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) int windowMillis) {
- if (windowMillis < CALCULATION_WINDOW_MILLIS_MIN
- || windowMillis > CALCULATION_WINDOW_MILLIS_MAX) {
- throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
- }
- mInternal.calculationWindowMillis = windowMillis;
- }
-
- /**
* Gets the headroom calculation window size in milliseconds.
* <p>
- * This will return the default value chosen by the device if the params is not set.
+ * This will return the default value chosen by the device if not set.
*/
- public @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) long getCalculationWindowMillis() {
+ public long getCalculationWindowMillis() {
return mInternal.calculationWindowMillis;
}
/**
- * Sets the thread TIDs to track.
- * <p>
- * The TIDs should belong to the same of the process that will the headroom call. And they
- * should not have different core affinity.
+ * Gets the TIDs to track.
* <p>
- * If not set, the headroom will be based on the PID of the process making the call.
- *
- * @param tids non-empty list of TIDs, maximum 5.
- * @throws IllegalArgumentException if the list size is not in allowed range or TID is not
- * positive.
+ * This will return a copy of the TIDs in the params, or null if the params is not set.
*/
- public void setTids(@NonNull int... tids) {
- if (tids.length == 0 || tids.length > MAX_TID_COUNT) {
- throw new IllegalArgumentException("Invalid number of TIDs: " + tids.length);
- }
- for (int tid : tids) {
- if (tid <= 0) {
- throw new IllegalArgumentException("Invalid TID: " + tid);
- }
- }
- mInternal.tids = Arrays.copyOf(tids, tids.length);
+ @NonNull
+ public int[] getTids() {
+ return mInternal.tids == null ? null : Arrays.copyOf(mInternal.tids, mInternal.tids.length);
}
- /**
- * @hide
- */
- public CpuHeadroomParamsInternal getInternal() {
- return mInternal;
+ @Override
+ public String toString() {
+ return "CpuHeadroomParams{"
+ + "calculationType=" + mInternal.calculationType
+ + ", calculationWindowMillis=" + mInternal.calculationWindowMillis
+ + ", tids=" + Arrays.toString(mInternal.tids)
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CpuHeadroomParams that = (CpuHeadroomParams) o;
+ return mInternal.equals(that.mInternal);
+ }
+
+ @Override
+ public int hashCode() {
+ return mInternal.hashCode();
}
}
diff --git a/core/java/android/os/GpuHeadroomParams.java b/core/java/android/os/GpuHeadroomParams.java
index 126ee8cce3d0..5c5e7bb5ccaf 100644
--- a/core/java/android/os/GpuHeadroomParams.java
+++ b/core/java/android/os/GpuHeadroomParams.java
@@ -19,6 +19,7 @@ package android.os;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
+import android.annotation.NonNull;
import android.os.health.SystemHealthManager;
import java.lang.annotation.Retention;
@@ -26,15 +27,11 @@ import java.lang.annotation.RetentionPolicy;
/**
* Headroom request params used by {@link SystemHealthManager#getGpuHeadroom(GpuHeadroomParams)}.
+ *
+ * <p>This class is immutable and one should use the {@link Builder} to build a new instance.
*/
@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
public final class GpuHeadroomParams {
- final GpuHeadroomParamsInternal mInternal;
-
- public GpuHeadroomParams() {
- mInternal = new GpuHeadroomParamsInternal();
- }
-
/** @hide */
@IntDef(flag = false, prefix = {"GPU_HEADROOM_CALCULATION_TYPE_"}, value = {
GPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
@@ -45,82 +42,153 @@ public final class GpuHeadroomParams {
}
/**
- * Calculates the headroom based on minimum value over a device-defined window.
+ * The headroom calculation type bases on minimum value over a specified window.
*/
public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
/**
- * Calculates the headroom based on average value over a device-defined window.
+ * The headroom calculation type bases on average value over a specified window.
*/
public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
- private static final int CALCULATION_WINDOW_MILLIS_MIN = 50;
- private static final int CALCULATION_WINDOW_MILLIS_MAX = 10000;
+ /**
+ * The minimum size of the window to compute the headroom over.
+ */
+ public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
/**
- * Sets the headroom calculation type.
- * <p>
- *
- * @throws IllegalArgumentException if the type is invalid.
+ * The maximum size of the window to compute the headroom over.
+ */
+ public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
+
+ /**
+ * @hide
+ */
+ public final GpuHeadroomParamsInternal mInternal;
+
+ /**
+ * @hide
*/
- public void setCalculationType(@GpuHeadroomCalculationType int calculationType) {
- switch (calculationType) {
- case GPU_HEADROOM_CALCULATION_TYPE_MIN:
- case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
- mInternal.calculationType = (byte) calculationType;
- return;
+ private GpuHeadroomParams() {
+ mInternal = new GpuHeadroomParamsInternal();
+ }
+
+ public static final class Builder {
+ private int mCalculationType = -1;
+ private int mCalculationWindowMillis = -1;
+
+ public Builder() {
+ }
+
+ /**
+ * Returns a new builder with the same values as this object.
+ */
+ public Builder(@NonNull GpuHeadroomParams params) {
+ if (params.mInternal.calculationType >= 0) {
+ mCalculationType = params.mInternal.calculationType;
+ }
+ if (params.mInternal.calculationWindowMillis >= 0) {
+ mCalculationWindowMillis = params.mInternal.calculationWindowMillis;
+ }
+ }
+
+ /**
+ * Sets the headroom calculation type.
+ * <p>
+ *
+ * @throws IllegalArgumentException if the type is invalid.
+ */
+ @NonNull
+ public Builder setCalculationType(
+ @GpuHeadroomCalculationType int calculationType) {
+ switch (calculationType) {
+ case GPU_HEADROOM_CALCULATION_TYPE_MIN:
+ case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE: {
+ mCalculationType = calculationType;
+ return this;
+ }
+ }
+ throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+ }
+
+ /**
+ * Sets the headroom calculation window size in milliseconds.
+ * <p>
+ *
+ * @param windowMillis the window size in milliseconds ranges from
+ * {@link SystemHealthManager#getGpuHeadroomCalculationWindowRange()}.
+ * The smaller the window size, the larger fluctuation in the headroom
+ * value should be expected. The default value can be retrieved from
+ * the {@link GpuHeadroomParams#getCalculationWindowMillis}. The device
+ * will try to use the closest feasible window size to this param.
+ * @throws IllegalArgumentException if the window is invalid.
+ */
+ @NonNull
+ public Builder setCalculationWindowMillis(@IntRange(from = 1) int windowMillis) {
+ if (windowMillis <= 0) {
+ throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
+ }
+ mCalculationWindowMillis = windowMillis;
+ return this;
+ }
+
+ /**
+ * Builds the {@link GpuHeadroomParams} object.
+ */
+ @NonNull
+ public GpuHeadroomParams build() {
+ GpuHeadroomParams params = new GpuHeadroomParams();
+ if (mCalculationType >= 0) {
+ params.mInternal.calculationType = (byte) mCalculationType;
+ }
+ if (mCalculationWindowMillis >= 0) {
+ params.mInternal.calculationWindowMillis = mCalculationWindowMillis;
+ }
+ return params;
}
- throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
}
/**
* Gets the headroom calculation type.
- * Default to {@link #GPU_HEADROOM_CALCULATION_TYPE_MIN} if the params is not set.
+ * <p>
+ * This will return the default value chosen by the device if not set.
*/
public @GpuHeadroomCalculationType int getCalculationType() {
@GpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
- case GPU_HEADROOM_CALCULATION_TYPE_MIN, GPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
- mInternal.calculationType;
+ case GPU_HEADROOM_CALCULATION_TYPE_MIN,
+ GPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> mInternal.calculationType;
default -> GPU_HEADROOM_CALCULATION_TYPE_MIN;
};
return validatedType;
}
/**
- * Sets the headroom calculation window size in milliseconds.
- * <p>
- *
- * @param windowMillis the window size in milliseconds ranges from [50, 10000]. The smaller the
- * window size, the larger fluctuation in the headroom value should be
- * expected. The default value can be retrieved from the
- * {@link #getCalculationWindowMillis}. The device will try to use the
- * closest feasible window size to this param.
- * @throws IllegalArgumentException if the window is invalid.
- */
- public void setCalculationWindowMillis(
- @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) int windowMillis) {
- if (windowMillis < CALCULATION_WINDOW_MILLIS_MIN
- || windowMillis > CALCULATION_WINDOW_MILLIS_MAX) {
- throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
- }
- mInternal.calculationWindowMillis = windowMillis;
- }
-
- /**
* Gets the headroom calculation window size in milliseconds.
* <p>
* This will return the default value chosen by the device if not set.
*/
- public @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) int getCalculationWindowMillis() {
+ public int getCalculationWindowMillis() {
return mInternal.calculationWindowMillis;
}
- /**
- * @hide
- */
- public GpuHeadroomParamsInternal getInternal() {
- return mInternal;
+ @Override
+ public String toString() {
+ return "GpuHeadroomParams{"
+ + "calculationType=" + mInternal.calculationType
+ + ", calculationWindowMillis=" + mInternal.calculationWindowMillis
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GpuHeadroomParams that = (GpuHeadroomParams) o;
+ return mInternal.equals(that.mInternal);
+ }
+
+ @Override
+ public int hashCode() {
+ return mInternal.hashCode();
}
}
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 5128e9173358..243254526f60 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -72,6 +72,7 @@ interface IHintManager {
parcelable HintManagerClientData {
int powerHalVersion;
int maxGraphicsPipelineThreads;
+ int maxCpuHeadroomThreads;
long preferredRateNanos;
SupportInfo supportInfo;
}
@@ -88,4 +89,6 @@ interface IHintManager {
* passing back a bundle of support and configuration information.
*/
HintManagerClientData registerClient(in IHintManagerClient client);
+
+ HintManagerClientData getClientData();
}
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index cd79e416531a..a1e9cf25e3e1 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -18,6 +18,7 @@ package android.os.health;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
@@ -42,6 +43,8 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.SynchronousResultReceiver;
+import android.util.Pair;
+import android.util.Slog;
import com.android.internal.app.IBatteryStats;
import com.android.server.power.optimization.Flags;
@@ -69,15 +72,41 @@ import java.util.function.Consumer;
* plugged in (e.g. using {@link android.app.job.JobScheduler JobScheduler}), and
* while that can affect charging rates, it is still preferable to actually draining
* the battery.
+ * <p>
+ * <b>CPU/GPU Usage</b><br>
+ * CPU/GPU headroom APIs are designed to be best used by applications with consistent and intense
+ * workload such as games to query the remaining capacity headroom over a short period and perform
+ * optimization accordingly. Due to the nature of the fast job scheduling and frequency scaling of
+ * CPU and GPU, the headroom by nature will have "TOCTOU" problem which makes it less suitable for
+ * apps with inconsistent or low workload to take any useful action but simply monitoring. And to
+ * avoid oscillation it's not recommended to adjust workload too frequent (on each polling request)
+ * or too aggressively. As the headroom calculation is more based on reflecting past history usage
+ * than predicting future capacity. Take game as an example, if the API returns CPU headroom of 0 in
+ * one scenario (especially if it's constant across multiple calls), or some value significantly
+ * smaller than other scenarios, then it can reason that the recent performance result is more CPU
+ * bottlenecked. Then reducing the CPU workload intensity can help reserve some headroom to handle
+ * the load variance better, which can result in less frame drops or smooth FPS value. On the other
+ * hand, if the API returns large CPU headroom constantly, the app can be more confident to increase
+ * the workload and expect higher possibility of device meeting its performance expectation.
+ * App can also use thermal APIs to read the current thermal status and headroom first, then poll
+ * the CPU and GPU headroom if the device is (about to) getting thermal throttled. If the CPU/GPU
+ * headrooms provide enough significance such as one valued at 0 while the other at 100, then it can
+ * be used to infer that reducing CPU workload could be more efficient to cool down the device.
+ * There is a caveat that the power controller may scale down the frequency of the CPU and GPU due
+ * to thermal and other reasons, which can result in a higher than usual percentage usage of the
+ * capacity.
*/
@SystemService(Context.SYSTEM_HEALTH_SERVICE)
public class SystemHealthManager {
+ private static final String TAG = "SystemHealthManager";
@NonNull
private final IBatteryStats mBatteryStats;
@Nullable
private final IPowerStatsService mPowerStats;
@Nullable
private final IHintManager mHintManager;
+ @Nullable
+ private final IHintManager.HintManagerClientData mHintManagerClientData;
private List<PowerMonitor> mPowerMonitorsInfo;
private final Object mPowerMonitorsLock = new Object();
private static final long TAKE_UID_SNAPSHOT_TIMEOUT_MILLIS = 10_000;
@@ -109,29 +138,72 @@ public class SystemHealthManager {
mBatteryStats = batteryStats;
mPowerStats = powerStats;
mHintManager = hintManager;
+ IHintManager.HintManagerClientData data = null;
+ if (mHintManager != null) {
+ try {
+ data = mHintManager.getClientData();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get hint manager client data", e);
+ }
+ }
+ mHintManagerClientData = data;
}
/**
- * Provides an estimate of global available CPU headroom.
+ * Provides an estimate of available CPU capacity headroom of the device.
+ * <p>
+ * The value can be used by the calling application to determine if the workload was CPU bound
+ * and then take action accordingly to ensure that the workload can be completed smoothly. It
+ * can also be used with the thermal status and headroom to determine if reducing the CPU bound
+ * workload can help reduce the device temperature to avoid thermal throttling.
* <p>
+ * If the params are valid, each call will perform at least one synchronous binder transaction
+ * that can take more than 1ms. So it's not recommended to call or wait for this on critical
+ * threads. Some devices may implement this as an on-demand API with lazy initialization, so the
+ * caller should expect higher latency when making the first call (especially with non-default
+ * params) since app starts or after changing params, as the device may need to change its data
+ * collection.
*
- * @param params params to customize the CPU headroom calculation, null to use default params.
- * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
- * A valid value is ranged from [0, 100], where 0 indicates no more CPU resources can be
- * granted.
+ * @param params params to customize the CPU headroom calculation, or null to use default.
+ * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable due to
+ * server error or not enough user CPU workload.
+ * Each valid value ranges from [0, 100], where 0 indicates no more cpu resources can be
+ * granted
* @throws UnsupportedOperationException if the API is unsupported.
+ * @throws IllegalArgumentException if the params are invalid.
* @throws SecurityException if the TIDs of the params don't belong to the same process.
* @throws IllegalStateException if the TIDs of the params don't have the same affinity setting.
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public @FloatRange(from = 0f, to = 100f) float getCpuHeadroom(
@Nullable CpuHeadroomParams params) {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
throw new UnsupportedOperationException();
}
+ if (params != null) {
+ if (params.mInternal.tids != null && (params.mInternal.tids.length == 0
+ || params.mInternal.tids.length
+ > mHintManagerClientData.maxCpuHeadroomThreads)) {
+ throw new IllegalArgumentException(
+ "Invalid number of TIDs: " + params.mInternal.tids.length);
+ }
+ if (params.mInternal.calculationWindowMillis
+ < mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis
+ || params.mInternal.calculationWindowMillis
+ > mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis) {
+ throw new IllegalArgumentException(
+ "Invalid calculation window: "
+ + params.mInternal.calculationWindowMillis + ", expect range: ["
+ + mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis
+ + ", "
+ + mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis
+ + "]");
+ }
+ }
try {
final CpuHeadroomResult ret = mHintManager.getCpuHeadroom(
- params != null ? params.getInternal() : new CpuHeadroomParamsInternal());
+ params != null ? params.mInternal : new CpuHeadroomParamsInternal());
if (ret == null || ret.getTag() != CpuHeadroomResult.globalHeadroom) {
return Float.NaN;
}
@@ -141,27 +213,69 @@ public class SystemHealthManager {
}
}
-
+ /**
+ * Gets the maximum number of TIDs this device supports for getting CPU headroom.
+ * <p>
+ * See {@link CpuHeadroomParams#setTids(int...)}.
+ *
+ * @return the maximum size of TIDs supported
+ * @throws UnsupportedOperationException if the CPU headroom API is unsupported.
+ */
+ @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+ public @IntRange(from = 1) int getMaxCpuHeadroomTidsSize() {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
+ throw new UnsupportedOperationException();
+ }
+ return mHintManagerClientData.maxCpuHeadroomThreads;
+ }
/**
- * Provides an estimate of global available GPU headroom of the device.
+ * Provides an estimate of available GPU capacity headroom of the device.
+ * <p>
+ * The value can be used by the calling application to determine if the workload was GPU bound
+ * and then take action accordingly to ensure that the workload can be completed smoothly. It
+ * can also be used with the thermal status and headroom to determine if reducing the GPU bound
+ * workload can help reduce the device temperature to avoid thermal throttling.
* <p>
+ * If the params are valid, each call will perform at least one synchronous binder transaction
+ * that can take more than 1ms. So it's not recommended to call or wait for this on critical
+ * threads. Some devices may implement this as an on-demand API with lazy initialization, so the
+ * caller should expect higher latency when making the first call (especially with non-default
+ * params) since app starts or after changing params, as the device may need to change its data
+ * collection.
*
- * @param params params to customize the GPU headroom calculation, null to use default params.
+ * @param params params to customize the GPU headroom calculation, or null to use default.
* @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
- * A valid value is ranged from [0, 100], where 0 indicates no more GPU resources can be
+ * Each valid value ranges from [0, 100], where 0 indicates no more cpu resources can be
* granted.
* @throws UnsupportedOperationException if the API is unsupported.
+ * @throws IllegalArgumentException if the params are invalid.
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public @FloatRange(from = 0f, to = 100f) float getGpuHeadroom(
@Nullable GpuHeadroomParams params) {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
throw new UnsupportedOperationException();
}
+ if (params != null) {
+ if (params.mInternal.calculationWindowMillis
+ < mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis
+ || params.mInternal.calculationWindowMillis
+ > mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis) {
+ throw new IllegalArgumentException(
+ "Invalid calculation window: "
+ + params.mInternal.calculationWindowMillis + ", expect range: ["
+ + mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis
+ + ", "
+ + mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis
+ + "]");
+ }
+ }
try {
final GpuHeadroomResult ret = mHintManager.getGpuHeadroom(
- params != null ? params.getInternal() : new GpuHeadroomParamsInternal());
+ params != null ? params.mInternal : new GpuHeadroomParamsInternal());
if (ret == null || ret.getTag() != GpuHeadroomResult.globalHeadroom) {
return Float.NaN;
}
@@ -172,7 +286,51 @@ public class SystemHealthManager {
}
/**
- * Minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
+ * Gets the range of the calculation window size for CPU headroom.
+ * <p>
+ * In API version 36, the range will be a superset of [50, 10000].
+ * <p>
+ * See {@link CpuHeadroomParams#setCalculationWindowMillis(int)}.
+ *
+ * @return the range of the calculation window size supported in milliseconds.
+ * @throws UnsupportedOperationException if the CPU headroom API is unsupported.
+ */
+ @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+ @NonNull
+ public Pair<Integer, Integer> getCpuHeadroomCalculationWindowRange() {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
+ throw new UnsupportedOperationException();
+ }
+ return new Pair<>(
+ mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis,
+ mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis);
+ }
+
+ /**
+ * Gets the range of the calculation window size for GPU headroom.
+ * <p>
+ * In API version 36, the range will be a superset of [50, 10000].
+ * <p>
+ * See {@link GpuHeadroomParams#setCalculationWindowMillis(int)}.
+ *
+ * @return the range of the calculation window size supported in milliseconds.
+ * @throws UnsupportedOperationException if the GPU headroom API is unsupported.
+ */
+ @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+ @NonNull
+ public Pair<Integer, Integer> getGpuHeadroomCalculationWindowRange() {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
+ throw new UnsupportedOperationException();
+ }
+ return new Pair<>(
+ mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis,
+ mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis);
+ }
+
+ /**
+ * Gets minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
* milliseconds.
* <p>
* The {@link #getCpuHeadroom(CpuHeadroomParams)} API may return cached result if called more
@@ -182,7 +340,8 @@ public class SystemHealthManager {
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public long getCpuHeadroomMinIntervalMillis() {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
throw new UnsupportedOperationException();
}
try {
@@ -193,7 +352,7 @@ public class SystemHealthManager {
}
/**
- * Minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
+ * Gets minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
* milliseconds.
* <p>
* The {@link #getGpuHeadroom(GpuHeadroomParams)} API may return cached result if called more
@@ -203,7 +362,8 @@ public class SystemHealthManager {
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public long getGpuHeadroomMinIntervalMillis() {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
throw new UnsupportedOperationException();
}
try {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index df12a68a8f22..c94526bcdcd7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12223,49 +12223,6 @@ public final class Settings {
"back_gesture_inset_scale_right";
/**
- * Indicates whether the trackpad back gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_BACK_ENABLED = "trackpad_gesture_back_enabled";
-
- /**
- * Indicates whether the trackpad home gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_HOME_ENABLED = "trackpad_gesture_home_enabled";
-
- /**
- * Indicates whether the trackpad overview gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_OVERVIEW_ENABLED =
- "trackpad_gesture_overview_enabled";
-
- /**
- * Indicates whether the trackpad notification gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_NOTIFICATION_ENABLED =
- "trackpad_gesture_notification_enabled";
-
- /**
- * Indicates whether the trackpad quick switch gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED =
- "trackpad_gesture_quick_switch_enabled";
-
- /**
* Current provider of proximity-based sharing services.
* Default value in @string/config_defaultNearbySharingComponent.
* No VALIDATOR as this setting will not be backed up.
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index d8a88b83df99..d313fc905a1d 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -100,13 +100,6 @@ public class FeatureFlagUtils {
public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD = "settings_new_keyboard_trackpad";
/**
- * Enable trackpad gesture settings UI
- * @hide
- */
- public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE =
- "settings_new_keyboard_trackpad_gesture";
-
- /**
* Enable the new pages which is implemented with SPA.
* @hide
*/
@@ -211,7 +204,6 @@ public class FeatureFlagUtils {
DEFAULT_FLAGS.put(SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, "true");
DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true");
DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true");
- DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "true");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_PHASE2, "false");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_METRICS, "true");
@@ -237,7 +229,6 @@ public class FeatureFlagUtils {
PERSISTENT_FLAGS.add(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME);
PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD);
- PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE);
PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA);
PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA_PHASE2);
PERSISTENT_FLAGS.add(SETTINGS_PREFER_ACCESSIBILITY_MENU_IN_SYSTEM);
diff --git a/core/java/android/util/SystemPropertySetter.java b/core/java/android/util/SystemPropertySetter.java
new file mode 100644
index 000000000000..bf18f753231e
--- /dev/null
+++ b/core/java/android/util/SystemPropertySetter.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemProperties;
+
+/**
+ * This class provides a single, static function to set a system property. The function retries on
+ * error. System properties should be reliable but there have been reports of failures in the set()
+ * command on lower-end devices. Clients may want to use this method instead of calling
+ * {@link SystemProperties.set} directly.
+ * @hide
+ */
+public class SystemPropertySetter {
+
+ /**
+ * The default retryDelayMs for {@link #setWithRetry}. This value has been found to give
+ * reasonable behavior in the field.
+ */
+ public static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
+
+ /**
+ * The default retryLimit for {@link #setWithRetry}. This value has been found to give
+ * reasonable behavior in the field.
+ */
+ public static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
+
+ /**
+ * Set the value for the given {@code key} to {@code val}. This method retries using the
+ * standard parameters, above, if the native method throws a RuntimeException.
+ *
+ * @param key The name of the property to be set.
+ * @param val The new string value of the property.
+ * @throws IllegalArgumentException for non read-only properties if the {@code val} exceeds
+ * 91 characters.
+ * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+ * SELinux. libc will log the underlying reason.
+ */
+ public static void setWithRetry(@NonNull String key, @Nullable String val) {
+ setWithRetry(key, val,PROPERTY_FAILURE_RETRY_DELAY_MILLIS, PROPERTY_FAILURE_RETRY_LIMIT);
+ }
+
+ /**
+ * Set the value for the given {@code key} to {@code val}. This method retries if the native
+ * method throws a RuntimeException. If the {@code maxRetry} count is exceeded, the method
+ * throws the first RuntimeException that was seen.
+ *
+ * @param key The name of the property to be set.
+ * @param val The new string value of the property.
+ * @param maxRetry The maximum number of times; must be non-negative.
+ * @param retryDelayMs The number of milliseconds to wait between retries; must be positive.
+ * @throws IllegalArgumentException for non read-only properties if the {@code val} exceeds
+ * 91 characters, or if the retry parameters are invalid.
+ * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+ * SELinux. libc will log the underlying reason.
+ */
+ public static void setWithRetry(@NonNull String key, @Nullable String val, int maxRetry,
+ long retryDelayMs) {
+ if (maxRetry < 0) {
+ throw new IllegalArgumentException("invalid retry count: " + maxRetry);
+ }
+ if (retryDelayMs <= 0) {
+ throw new IllegalArgumentException("invalid retry delay: " + retryDelayMs);
+ }
+
+ RuntimeException failure = null;
+ for (int attempt = 0; attempt < maxRetry; attempt++) {
+ try {
+ SystemProperties.set(key, val);
+ return;
+ } catch (RuntimeException e) {
+ if (failure == null) {
+ failure = e;
+ }
+ try {
+ Thread.sleep(retryDelayMs);
+ } catch (InterruptedException x) {
+ // Ignore this exception. The desired delay is only approximate and
+ // there is no issue if the sleep sometimes terminates early.
+ }
+ }
+ }
+ // This point is reached only if SystemProperties.set() fails at least once.
+ // Rethrow the first exception that was received.
+ throw failure;
+ }
+}
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 19c98a2f12b7..cdfa7c81969b 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -34,6 +34,9 @@ import java.lang.annotation.RetentionPolicy;
/**
* Interface to control windows that generate insets.
+ *
+ * For guidance, see <a href="https://developer.android.com/develop/ui/views/layout/immersive">
+ * Hide system bars for immersive mode</a>.
*/
public interface WindowInsetsController {
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 617e4762ebf0..f50ea9106a61 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -50,7 +50,6 @@ import android.window.TrustedPresentationThresholds;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.policy.PhoneWindow;
import com.android.internal.util.FastPrintWriter;
import java.io.FileDescriptor;
@@ -376,8 +375,7 @@ public final class WindowManagerGlobal {
if (context != null && wparams.type > LAST_APPLICATION_WINDOW) {
final TypedArray styles = context.obtainStyledAttributes(R.styleable.Window);
- if (PhoneWindow.isOptingOutEdgeToEdgeEnforcement(
- context.getApplicationInfo(), true /* local */, styles)) {
+ if (styles.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)) {
wparams.privateFlags |= PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE;
}
styles.recycle();
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index b3bd92b37357..c871d568e625 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -45,3 +45,10 @@ flag {
description: "If true, the APIs to manage content protection device policy will be enabled."
bug: "319477846"
}
+
+flag {
+ name: "exported_settings_activity_enabled"
+ namespace: "content_protection"
+ description: "If true, the content protection Settings Activity will be exported for launching externally."
+ bug: "385310141"
+}
diff --git a/core/java/android/widget/flags/flags.aconfig b/core/java/android/widget/flags/flags.aconfig
index 88eb0438ec4b..83a4c8676a38 100644
--- a/core/java/android/widget/flags/flags.aconfig
+++ b/core/java/android/widget/flags/flags.aconfig
@@ -2,7 +2,7 @@ package: "android.widget.flags"
container: "system"
flag {
name: "enable_fading_view_group"
- namespace: "system_performance"
+ namespace: "wear_systems"
description: "FRP screen during OOBE must have fading and scaling animation in Wear Watches"
bug: "348515581"
metadata {
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 091f86ec9234..a8641326b1f2 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -423,7 +423,7 @@ flag {
flag {
name: "port_window_size_animation"
- namespace: "systemui"
+ namespace: "windowing_frontend"
description: "Port window-resize animation from legacy to shell"
bug: "384976265"
}
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index fc415377f1ee..d1adfc95461d 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -246,8 +246,19 @@ public class Cuj {
*/
public static final int CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW = 119;
+ /**
+ * Track moving overview task to desktop interaction from overview menu.
+ *
+ * <p> Tracking starts when the overview task is moved to desktop via the overview menu.
+ * Tracking finishes when successfully made a call to `IDesktopMode.moveToDesktop`,
+ * without waiting for transition completion.
+ * </p>
+ * TODO(b/387471509): Update the CUJ to wait for transition completion.
+ */
+ public static final int CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU = 120;
+
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
- @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW;
+ @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU;
/** @hide */
@IntDef({
@@ -358,7 +369,8 @@ public class Cuj {
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE,
CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
CUJ_DESKTOP_MODE_SNAP_RESIZE,
- CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW
+ CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW,
+ CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {}
@@ -480,6 +492,7 @@ public class Cuj {
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_SNAP_RESIZE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_SNAP_RESIZE;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_UNMAXIMIZE_WINDOW;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU;
}
private Cuj() {
@@ -714,6 +727,8 @@ public class Cuj {
return "DESKTOP_MODE_SNAP_RESIZE";
case CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW:
return "DESKTOP_MODE_UNMAXIMIZE_WINDOW";
+ case CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU:
+ return "DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 67e358a47cce..e0529b339d4a 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -184,14 +184,6 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
private static final long ENFORCE_EDGE_TO_EDGE = 309578419;
/**
- * Disable opting out the edge-to-edge enforcement.
- * {@link Build.VERSION_CODES#BAKLAVA} or above.
- */
- @ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
- private static final long DISABLE_OPT_OUT_EDGE_TO_EDGE = 377864165;
-
- /**
* Override the layout in display cutout mode behavior. This will only apply if the edge to edge
* is not enforced.
*/
@@ -458,7 +450,7 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
*/
public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local,
TypedArray windowStyle) {
- return !isOptingOutEdgeToEdgeEnforcement(info, local, windowStyle)
+ return !windowStyle.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)
&& (info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
|| (Flags.enforceEdgeToEdge() && (local
// Calling this doesn't require a permission.
@@ -467,26 +459,6 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
: info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))));
}
- /**
- * Returns whether the given application is opting out edge-to-edge enforcement.
- *
- * @param info The application to query.
- * @param local Whether this is called from the process of the given application.
- * @param windowStyle The style of the window.
- * @return {@code true} if the edge-to-edge enforcement is opting out. Otherwise, {@code false}.
- */
- public static boolean isOptingOutEdgeToEdgeEnforcement(ApplicationInfo info, boolean local,
- TypedArray windowStyle) {
- final boolean disabled = (Flags.disableOptOutEdgeToEdge() && (local
- // Calling this doesn't require a permission.
- ? CompatChanges.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE)
- // Calling this requires permissions.
- : info.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE)));
- return !disabled && windowStyle.getBoolean(
- R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false /* default */);
-
- }
-
@Override
public final void setContainer(Window container) {
super.setContainer(container);
@@ -2514,7 +2486,6 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
TypedArray a = getWindowStyle();
WindowManager.LayoutParams params = getAttributes();
- ApplicationInfo appInfo = getContext().getApplicationInfo();
if (false) {
System.out.println("From style:");
@@ -2526,7 +2497,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
System.out.println(s);
}
- mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(appInfo, true /* local */, a);
+ mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(
+ getContext().getApplicationInfo(), true /* local */, a);
if (mEdgeToEdgeEnforced) {
getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
mDecorFitsSystemWindows = false;
@@ -2535,7 +2507,8 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
// mNavigationBarColor is not reset here because it might be used to draw the scrim.
}
if (CompatChanges.isChangeEnabled(OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE)
- && !isOptingOutEdgeToEdgeEnforcement(appInfo, true /* local */, a)) {
+ && !a.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement,
+ false /* defValue */)) {
getAttributes().privateFlags |= PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
}
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 8df3f2abcafd..e522b508b44b 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -94,14 +94,21 @@ public final class RavenwoodEnvironment {
/** Used for testing */
@Disabled
- @ChangeId public static final long TEST_COMPAT_ID_2 = 368131701L;
+ @ChangeId
+ public static final long TEST_COMPAT_ID_2 = 368131701L;
/** Used for testing */
@EnabledAfter(targetSdkVersion = S)
- @ChangeId public static final long TEST_COMPAT_ID_3 = 368131659L;
+ @ChangeId
+ public static final long TEST_COMPAT_ID_3 = 368131659L;
/** Used for testing */
@EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE)
- @ChangeId public static final long TEST_COMPAT_ID_4 = 368132057L;
+ @ChangeId
+ public static final long TEST_COMPAT_ID_4 = 368132057L;
+
+ /** Used for testing */
+ @ChangeId
+ public static final long TEST_COMPAT_ID_5 = 387558811L;
}
}
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 57bfc7086ed5..b2649a471b8a 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -198,7 +198,7 @@ static jobject NativeGetOverlayableMap(JNIEnv* env, jclass /*clazz*/, jlong ptr,
auto assetmanager = LockAndStartAssetManager(ptr);
const ScopedUtfChars package_name_utf8(env, package_name);
CHECK(package_name_utf8.c_str() != nullptr);
- const std::string std_package_name(package_name_utf8.c_str());
+ const std::string_view std_package_name(package_name_utf8.c_str());
const std::unordered_map<std::string, std::string>* map = nullptr;
assetmanager->ForEachPackage([&](const std::string& this_package_name, uint8_t package_id) {
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index cf81ba157bf9..96d34a0230b3 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -645,14 +645,8 @@ message SecureSettingsProto {
optional SettingProto theme_customization_overlay_packages = 75 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto trust_agents_initialized = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
- message TrackpadGesture {
- optional SettingProto trackpad_gesture_back_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_home_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_overview_enabled = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_notification_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_quick_switch_enabled = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
- }
- optional TrackpadGesture trackpad_gesture = 94;
+ reserved 94; // formerly trackpad_gesture
+ reserved "trackpad_gesture";
message Tts {
option (android.msg_privacy).dest = DEST_EXPLICIT;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index dc954718d623..ed021b64f7a0 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5405,10 +5405,9 @@
<permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME"
android:protectionLevel="signature" />
- <!-- @FlaggedApi("com.android.server.accessibility.motion_event_observing")
- @hide
- @TestApi
- Allows an accessibility service to observe motion events without consuming them. -->
+ <!-- @TestApi Allows an accessibility service to observe motion events
+ without consuming them.
+ @hide -->
<permission android:name="android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING"
android:protectionLevel="signature" />
diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml
index c8263c26f38f..1c367544c90a 100644
--- a/core/res/res/layout/notification_2025_expand_button.xml
+++ b/core/res/res/layout/notification_2025_expand_button.xml
@@ -22,6 +22,7 @@
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:contentDescription="@string/expand_button_content_description_collapsed"
+ android:padding="@dimen/notification_2025_margin"
>
<LinearLayout
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index c827dcb16584..f108ce5bd1b9 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -168,7 +168,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml
index ce38c1645fb1..6f3c15adb082 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_call.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml
@@ -70,7 +70,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 0021b8384847..bd17a3a0a74e 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -189,7 +189,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index f3e4ce13ff4b..edbebb17f825 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -193,7 +193,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_conversation.xml b/core/res/res/layout/notification_2025_template_conversation.xml
index 6be5a1cee7f9..24b6ad09d3f7 100644
--- a/core/res/res/layout/notification_2025_template_conversation.xml
+++ b/core/res/res/layout/notification_2025_template_conversation.xml
@@ -152,7 +152,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</LinearLayout>
</LinearLayout>
diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml
index 3f34eb3cbf0c..0c07053d428a 100644
--- a/core/res/res/layout/notification_2025_template_header.xml
+++ b/core/res/res/layout/notification_2025_template_header.xml
@@ -85,7 +85,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
android:layout_alignParentEnd="true" />
<include layout="@layout/notification_close_button"
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index bb5380e1312d..06cd44e6544d 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
<!-- Arab Emirates -->
- <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568" />
+ <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568|999" />
<!-- Albania: 5 digits, known short codes listed -->
<shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -63,8 +63,8 @@
<!-- Burkina Faso: 1-4 digits (standard system default, not country specific) -->
<shortcode country="bf" pattern="\\d{1,4}" free="3558" />
- <!-- Bulgaria: 4-5 digits, plus EU -->
- <shortcode country="bg" pattern="\\d{4,5}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490" />
+ <!-- Bulgaria: 4-6 digits, plus EU -->
+ <shortcode country="bg" pattern="\\d{4,6}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490|162055" />
<!-- Bahrain: 1-5 digits (standard system default, not country specific) -->
<shortcode country="bh" pattern="\\d{1,5}" free="81181|85999" />
@@ -81,18 +81,21 @@
<!-- Canada: 5-6 digits -->
<shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677|24470" />
+ <!-- DR Congo: 1-6 digits, known premium codes listed -->
+ <shortcode country="cd" pattern="\\d{1,6}" free="444123" />
+
<!-- Switzerland: 3-5 digits: http://www.swisscom.ch/fxres/kmu/thirdpartybusiness_code_of_conduct_en.pdf -->
<shortcode country="ch" pattern="[2-9]\\d{2,4}" premium="543|83111|30118" free="98765|30075|30047" />
<!-- Chile: 4-5 digits (not confirmed), known premium codes listed -->
- <shortcode country="cl" pattern="\\d{4,5}" free="9963|9240|1038" />
+ <shortcode country="cl" pattern="\\d{4,5}" free="9963|9240|1038|4848" />
<!-- China: premium shortcodes start with "1066", free shortcodes start with "1065":
http://clients.txtnation.com/entries/197192-china-premium-sms-short-code-requirements -->
<shortcode country="cn" premium="1066.*" free="1065.*" />
<!-- Colombia: 1-6 digits (not confirmed) -->
- <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289" />
+ <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289|890119" />
<!-- Costa Rica -->
<shortcode country="cr" pattern="\\d{1,6}" free="466453" />
@@ -116,6 +119,9 @@
<!-- Dominican Republic: 1-6 digits (standard system default, not country specific) -->
<shortcode country="do" pattern="\\d{1,6}" free="912892|912" />
+ <!-- Algeria: 1-5 digits, known premium codes listed -->
+ <shortcode country="dz" pattern="\\d{1,5}" free="63071" />
+
<!-- Ecuador: 1-6 digits (standard system default, not country specific) -->
<shortcode country="ec" pattern="\\d{1,6}" free="466453|18512" />
@@ -123,20 +129,23 @@
http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
<shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}|95034" />
- <!-- Egypt: 4-5 digits, known codes listed -->
- <shortcode country="eg" pattern="\\d{4,5}" free="1499|10020" />
+ <!-- Egypt: 4-6 digits, known codes listed -->
+ <shortcode country="eg" pattern="\\d{4,6}" free="1499|10020|100158" />
<!-- Spain: 5-6 digits: 25xxx, 27xxx, 280xx, 35xxx, 37xxx, 795xxx, 797xxx, 995xxx, 997xxx, plus EU.
http://www.legallink.es/?q=en/content/which-current-regulatory-status-premium-rate-services-spain -->
<shortcode country="es" premium="[23][57]\\d{3}|280\\d{2}|[79]9[57]\\d{3}" free="116\\d{3}|22791|222145|22189" />
+ <!-- Ethiopia: 1-4 digits, known codes listed -->
+ <shortcode country="et" pattern="\\d{1,4}" free="8527" />
+
<!-- Finland: 5-6 digits, premium 0600, 0700: http://en.wikipedia.org/wiki/Telephone_numbers_in_Finland -->
<shortcode country="fi" pattern="\\d{5,6}" premium="0600.*|0700.*|171(?:59|63)" free="116\\d{3}|14789|17110" />
<!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU:
http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements,
visual voicemail code for Orange: 21101 -->
- <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033" />
+ <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033|21727" />
<!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU:
http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf,
@@ -179,17 +188,17 @@
<shortcode country="il" pattern="\\d{1,5}" premium="4422|4545" free="37477|6681" />
<!-- Iran: 4-8 digits, known premium codes listed -->
- <shortcode country="ir" pattern="\\d{4,8}" free="700791|700792|100016|30008360" />
+ <shortcode country="ir" pattern="\\d{4,8}" free="700792|100016|30008360" />
<!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU:
https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
<shortcode country="it" pattern="\\d{5}" premium="44[0-4]\\d{2}|47[0-4]\\d{2}|48[0-4]\\d{2}|44[5-9]\\d{4}|47[5-9]\\d{4}|48[5-9]\\d{4}|455\\d{2}|499\\d{2}" free="116\\d{3}|4112503|40\\d{0,12}" standard="430\\d{2}|431\\d{2}|434\\d{4}|435\\d{4}|439\\d{7}" />
<!-- Jordan: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="jo" pattern="\\d{1,5}" free="99066" />
+ <shortcode country="jo" pattern="\\d{1,5}" free="99066|99390" />
<!-- Japan: 8083 used by SOFTBANK_DCB_2 -->
- <shortcode country="jp" pattern="\\d{1,5}" free="8083" />
+ <shortcode country="jp" pattern="\\d{1,9}" free="8083|00050320" />
<!-- Kenya: 5 digits, known premium codes listed -->
<shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023|24088|23054" />
@@ -206,6 +215,9 @@
<!-- Kuwait: 1-5 digits (standard system default, not country specific) -->
<shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991|50976|7112" />
+ <!-- Lesotho: 4-5 digits, known codes listed -->
+ <shortcode country="ls" pattern="\\d{4,5}" free="32012" />
+
<!-- Lithuania: 3-5 digits, known premium codes listed, plus EU -->
<shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}|1399|1324" />
@@ -222,11 +234,14 @@
<!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
<shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
+ <!-- Mali: 1-5 digits, known codes listed -->
+ <shortcode country="ml" pattern="\\d{1,5}" free="36098" />
+
<!-- Mongolia : 1-6 digits (standard system default, not country specific) -->
<shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" />
<!-- Malawi: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305" />
+ <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305|4326" />
<!-- Mozambique: 1-5 digits (standard system default, not country specific) -->
<shortcode country="mz" pattern="\\d{1,5}" free="1714" />
@@ -323,11 +338,14 @@
<!-- Tajikistan: 4 digits, known premium codes listed -->
<shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
+ <!-- Timor-Leste 1-5 digits, known codes listed -->
+ <shortcode country="tl" pattern="\\d{1,5}" free="46645" />
+
<!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324|15610" />
+ <shortcode country="tz" pattern="\\d{1,5}" free="15046|15324|15610" />
- <!-- Tunisia: 5 digits, known premium codes listed -->
- <shortcode country="tn" pattern="\\d{5}" free="85799" />
+ <!-- Tunisia: 1-6 digits, known premium codes listed -->
+ <shortcode country="tn" pattern="\\d{1,6}" free="85799|772024" />
<!-- Turkey -->
<shortcode country="tr" pattern="\\d{1,5}" free="7529|5528|6493|3193" />
@@ -336,7 +354,7 @@
<shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
<!-- Uganda(UG): 4 digits (standard system default, not country specific) -->
- <shortcode country="ug" pattern="\\d{4}" free="8000|8009" />
+ <shortcode country="ug" pattern="\\d{4}" free="8009" />
<!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
visual voicemail code for T-Mobile: 122 -->
@@ -349,7 +367,7 @@
<shortcode country="ve" pattern="\\d{1,6}" free="538352" />
<!-- Vietnam: 1-6 digits (standard system default, not country specific) -->
- <shortcode country="vn" pattern="\\d{1,6}" free="5001|9055|8079|90002|118989" />
+ <shortcode country="vn" pattern="\\d{1,6}" free="5001|9055|90002|118989|46645" />
<!-- Mayotte (French Territory): 1-5 digits (not confirmed) -->
<shortcode country="yt" pattern="\\d{1,5}" free="38600,36300,36303,959" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 3bbb9519d016..1b6746ca4b63 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -313,6 +313,7 @@ android_ravenwood_test {
"res/xml/power_profile_test_modem.xml",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
test_module_config {
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index 37ef6cba8814..939bf2ec4b0a 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -207,7 +207,8 @@ public class RegisteredServicesCacheTest extends AndroidTestCase {
final ComponentInfo info = new ComponentInfo();
info.applicationInfo = new ApplicationInfo();
info.applicationInfo.uid = uid;
- return new RegisteredServicesCache.ServiceInfo<>(type, info, null);
+ return new RegisteredServicesCache.ServiceInfo<>(type, info, null /* componentName */,
+ 0 /* lastUpdateTime */);
}
private void assertNotEmptyFileCreated(TestServicesCache cache, int userId) {
@@ -301,7 +302,7 @@ public class RegisteredServicesCacheTest extends AndroidTestCase {
@Override
protected ServiceInfo<TestServiceType> parseServiceInfo(
- ResolveInfo resolveInfo) throws XmlPullParserException, IOException {
+ ResolveInfo resolveInfo, int userId) throws XmlPullParserException, IOException {
int size = mServices.size();
for (int i = 0; i < size; i++) {
Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
diff --git a/core/tests/coretests/src/android/security/advancedprotection/OWNERS b/core/tests/coretests/src/android/security/advancedprotection/OWNERS
new file mode 100644
index 000000000000..9bf5e58c01a9
--- /dev/null
+++ b/core/tests/coretests/src/android/security/advancedprotection/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/base:main:/core/java/android/security/advancedprotection/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
index 5613caf4427e..d26bb35e5481 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -120,7 +120,9 @@ public class NotificationProgressBarTest {
List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
segments, points, progress, progressMax, isStyledByProgress);
- int fadedRed = 0x7FFF0000;
+ // Colors with 40% opacity
+ int fadedRed = 0x66FF0000;
+
List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
assertThat(parts).isEqualTo(expected);
@@ -199,8 +201,8 @@ public class NotificationProgressBarTest {
List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
segments, points, progress, progressMax, isStyledByProgress);
- // Colors with 50% opacity
- int fadedGreen = 0x7F00FF00;
+ // Colors with 40% opacity
+ int fadedGreen = 0x6600FF00;
List<Part> expected = new ArrayList<>(List.of(
new Segment(0.50f, Color.RED),
@@ -223,9 +225,9 @@ public class NotificationProgressBarTest {
int progressMax = 100;
boolean isStyledByProgress = true;
- // Colors with 50% opacity
- int fadedBlue = 0x7F0000FF;
- int fadedYellow = 0x7FFFFF00;
+ // Colors with 40% opacity
+ int fadedBlue = 0x660000FF;
+ int fadedYellow = 0x66FFFF00;
List<Part> expected = new ArrayList<>(List.of(
new Segment(0.15f, Color.BLUE),
@@ -261,9 +263,9 @@ public class NotificationProgressBarTest {
List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
segments, points, progress, progressMax, isStyledByProgress);
- // Colors with 50% opacity
- int fadedGreen = 0x7F00FF00;
- int fadedYellow = 0x7FFFFF00;
+ // Colors with 40% opacity
+ int fadedGreen = 0x6600FF00;
+ int fadedYellow = 0x66FFFF00;
List<Part> expected = new ArrayList<>(List.of(
new Segment(0.15f, Color.RED),
diff --git a/core/tests/systemproperties/Android.bp b/core/tests/systemproperties/Android.bp
index ed99a1f5cc4a..9197dec00d25 100644
--- a/core/tests/systemproperties/Android.bp
+++ b/core/tests/systemproperties/Android.bp
@@ -44,4 +44,5 @@ android_ravenwood_test {
"src/**/*.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index 7cf49ab5c376..5011f7ac1e7c 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -69,4 +69,5 @@ android_ravenwood_test {
"src/com/android/internal/util/**/*.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index eecf199a3ec2..03076c0940a4 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -35,7 +35,6 @@ android_app {
android_robolectric_test {
name: "WMShellRobolectricTests",
instrumentation_for: "WindowManagerShellRobolectric",
- upstream: true,
java_resource_dirs: [
"robolectric/config",
],
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index 4300e84e8044..2ca011bfe000 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -16,10 +16,12 @@
package com.android.wm.shell.shared;
+import static android.app.WindowConfiguration.windowingModeToString;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+
import android.annotation.IntDef;
import android.app.ActivityManager.RecentTaskInfo;
import android.app.TaskInfo;
-import android.app.WindowConfiguration;
import android.os.Parcel;
import android.os.Parcelable;
@@ -28,11 +30,14 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.shared.split.SplitBounds;
+import kotlin.collections.CollectionsKt;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Simple container for recent tasks which should be presented as a single task within the
@@ -43,11 +48,13 @@ public class GroupedTaskInfo implements Parcelable {
public static final int TYPE_FULLSCREEN = 1;
public static final int TYPE_SPLIT = 2;
public static final int TYPE_FREEFORM = 3;
+ public static final int TYPE_MIXED = 4;
@IntDef(prefix = {"TYPE_"}, value = {
TYPE_FULLSCREEN,
TYPE_SPLIT,
- TYPE_FREEFORM
+ TYPE_FREEFORM,
+ TYPE_MIXED
})
public @interface GroupType {}
@@ -64,7 +71,7 @@ public class GroupedTaskInfo implements Parcelable {
* TYPE_SPLIT: Contains the two split roots of each side
* TYPE_FREEFORM: Contains the set of tasks currently in freeform mode
*/
- @NonNull
+ @Nullable
protected final List<TaskInfo> mTasks;
/**
@@ -85,6 +92,14 @@ public class GroupedTaskInfo implements Parcelable {
protected final int[] mMinimizedTaskIds;
/**
+ * Only set for TYPE_MIXED.
+ *
+ * The mixed set of task infos in this group.
+ */
+ @Nullable
+ protected final List<GroupedTaskInfo> mGroupedTasks;
+
+ /**
* Create new for a stack of fullscreen tasks
*/
public static GroupedTaskInfo forFullscreenTasks(@NonNull TaskInfo task) {
@@ -111,18 +126,41 @@ public class GroupedTaskInfo implements Parcelable {
minimizedFreeformTasks.stream().mapToInt(i -> i).toArray());
}
+ /**
+ * Create new for a group of grouped task infos, those grouped task infos may not be mixed
+ * themselves (ie. multiple depths of mixed grouped task infos are not allowed).
+ */
+ public static GroupedTaskInfo forMixed(@NonNull List<GroupedTaskInfo> groupedTasks) {
+ if (groupedTasks.isEmpty()) {
+ throw new IllegalArgumentException("Expected non-empty grouped task list");
+ }
+ if (groupedTasks.stream().anyMatch(task -> task.mType == TYPE_MIXED)) {
+ throw new IllegalArgumentException("Unexpected grouped task list");
+ }
+ return new GroupedTaskInfo(groupedTasks);
+ }
+
private GroupedTaskInfo(
@NonNull List<TaskInfo> tasks,
@Nullable SplitBounds splitBounds,
@GroupType int type,
@Nullable int[] minimizedFreeformTaskIds) {
mTasks = tasks;
+ mGroupedTasks = null;
mSplitBounds = splitBounds;
mType = type;
mMinimizedTaskIds = minimizedFreeformTaskIds;
ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds);
}
+ private GroupedTaskInfo(@NonNull List<GroupedTaskInfo> groupedTasks) {
+ mTasks = null;
+ mGroupedTasks = groupedTasks;
+ mSplitBounds = null;
+ mType = TYPE_MIXED;
+ mMinimizedTaskIds = null;
+ }
+
private void ensureAllMinimizedIdsPresent(
@NonNull List<TaskInfo> tasks,
@Nullable int[] minimizedFreeformTaskIds) {
@@ -141,26 +179,47 @@ public class GroupedTaskInfo implements Parcelable {
for (int i = 0; i < numTasks; i++) {
mTasks.add(new TaskInfo(parcel));
}
+ mGroupedTasks = parcel.createTypedArrayList(GroupedTaskInfo.CREATOR);
mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR);
mType = parcel.readInt();
mMinimizedTaskIds = parcel.createIntArray();
}
/**
- * Get primary {@link RecentTaskInfo}
+ * If TYPE_MIXED, returns the root of the grouped tasks
+ * For all other types, returns this task itself
+ */
+ @NonNull
+ public GroupedTaskInfo getBaseGroupedTask() {
+ if (mType == TYPE_MIXED) {
+ return mGroupedTasks.getFirst();
+ }
+ return this;
+ }
+
+ /**
+ * Get primary {@link TaskInfo}.
+ *
+ * @throws IllegalStateException if the group is TYPE_MIXED.
*/
@NonNull
public TaskInfo getTaskInfo1() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No indexed tasks for a mixed task");
+ }
return mTasks.getFirst();
}
/**
- * Get secondary {@link RecentTaskInfo}.
+ * Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT.
*
- * Used in split screen.
+ * @throws IllegalStateException if the group is TYPE_MIXED.
*/
@Nullable
public TaskInfo getTaskInfo2() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No indexed tasks for a mixed task");
+ }
if (mTasks.size() > 1) {
return mTasks.get(1);
}
@@ -172,9 +231,7 @@ public class GroupedTaskInfo implements Parcelable {
*/
@Nullable
public TaskInfo getTaskById(int taskId) {
- return mTasks.stream()
- .filter(task -> task.taskId == taskId)
- .findFirst().orElse(null);
+ return CollectionsKt.firstOrNull(getTaskInfoList(), taskInfo -> taskInfo.taskId == taskId);
}
/**
@@ -182,35 +239,59 @@ public class GroupedTaskInfo implements Parcelable {
*/
@NonNull
public List<TaskInfo> getTaskInfoList() {
- return mTasks;
+ if (mType == TYPE_MIXED) {
+ return CollectionsKt.flatMap(mGroupedTasks, groupedTaskInfo -> groupedTaskInfo.mTasks);
+ } else {
+ return mTasks;
+ }
}
/**
* @return Whether this grouped task contains a task with the given {@code taskId}.
*/
public boolean containsTask(int taskId) {
- return mTasks.stream()
- .anyMatch((task -> task.taskId == taskId));
+ return getTaskById(taskId) != null;
}
/**
- * Return {@link SplitBounds} if this is a split screen entry or {@code null}
+ * Returns whether the group is of the given type, if this is a TYPE_MIXED group, then returns
+ * whether the root task info is of the given type.
+ */
+ public boolean isBaseType(@GroupType int type) {
+ return getBaseGroupedTask().mType == type;
+ }
+
+ /**
+ * Return {@link SplitBounds} if this is a split screen entry or {@code null}. Only valid for
+ * TYPE_SPLIT.
*/
@Nullable
public SplitBounds getSplitBounds() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No split bounds for a mixed task");
+ }
return mSplitBounds;
}
/**
- * Get type of this recents entry. One of {@link GroupType}
+ * Get type of this recents entry. One of {@link GroupType}.
+ * Note: This is deprecated, callers should use `isBaseType()` and not make assumptions about
+ * specific group types
*/
+ @Deprecated
@GroupType
public int getType() {
return mType;
}
+ /**
+ * Returns the set of minimized task ids, only valid for TYPE_FREEFORM.
+ */
@Nullable
public int[] getMinimizedTaskIds() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No minimized task ids for a mixed task");
+ }
return mMinimizedTaskIds;
}
@@ -222,67 +303,64 @@ public class GroupedTaskInfo implements Parcelable {
GroupedTaskInfo other = (GroupedTaskInfo) obj;
return mType == other.mType
&& Objects.equals(mTasks, other.mTasks)
+ && Objects.equals(mGroupedTasks, other.mGroupedTasks)
&& Objects.equals(mSplitBounds, other.mSplitBounds)
&& Arrays.equals(mMinimizedTaskIds, other.mMinimizedTaskIds);
}
@Override
public int hashCode() {
- return Objects.hash(mType, mTasks, mSplitBounds, Arrays.hashCode(mMinimizedTaskIds));
+ return Objects.hash(mType, mTasks, mGroupedTasks, mSplitBounds,
+ Arrays.hashCode(mMinimizedTaskIds));
}
@Override
public String toString() {
StringBuilder taskString = new StringBuilder();
- for (int i = 0; i < mTasks.size(); i++) {
- if (i == 0) {
- taskString.append("Task");
- } else {
- taskString.append(", Task");
+ if (mType == TYPE_MIXED) {
+ taskString.append("GroupedTasks=" + mGroupedTasks.stream()
+ .map(GroupedTaskInfo::toString)
+ .collect(Collectors.joining(",\n\t", "[\n\t", "\n]")));
+ } else {
+ taskString.append("Tasks=" + mTasks.stream()
+ .map(taskInfo -> getTaskInfoDumpString(taskInfo))
+ .collect(Collectors.joining(", ", "[", "]")));
+ if (mSplitBounds != null) {
+ taskString.append(", SplitBounds=").append(mSplitBounds);
}
- taskString.append(i + 1).append(": ").append(getTaskInfo(mTasks.get(i)));
+ taskString.append(", Type=" + typeToString(mType));
+ taskString.append(", Minimized Task IDs=" + Arrays.toString(mMinimizedTaskIds));
}
- if (mSplitBounds != null) {
- taskString.append(", SplitBounds: ").append(mSplitBounds);
- }
- taskString.append(", Type=");
- switch (mType) {
- case TYPE_FULLSCREEN:
- taskString.append("TYPE_FULLSCREEN");
- break;
- case TYPE_SPLIT:
- taskString.append("TYPE_SPLIT");
- break;
- case TYPE_FREEFORM:
- taskString.append("TYPE_FREEFORM");
- break;
- }
- taskString.append(", Minimized Task IDs: ");
- taskString.append(Arrays.toString(mMinimizedTaskIds));
return taskString.toString();
}
- private String getTaskInfo(TaskInfo taskInfo) {
+ private String getTaskInfoDumpString(TaskInfo taskInfo) {
if (taskInfo == null) {
return null;
}
+ final boolean isExcluded = (taskInfo.baseIntent.getFlags()
+ & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
return "id=" + taskInfo.taskId
- + " baseIntent=" +
- (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
- ? taskInfo.baseIntent.getComponent().flattenToString()
- : "null")
- + " winMode=" + WindowConfiguration.windowingModeToString(
- taskInfo.getWindowingMode());
+ + " winMode=" + windowingModeToString(taskInfo.getWindowingMode())
+ + " visReq=" + taskInfo.isVisibleRequested
+ + " vis=" + taskInfo.isVisible
+ + " excluded=" + isExcluded
+ + " baseIntent="
+ + (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
+ ? taskInfo.baseIntent.getComponent().flattenToShortString()
+ : "null");
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
// We don't use the parcel list methods because we want to only write the TaskInfo state
// and not the subclasses (Recents/RunningTaskInfo) whose fields are all deprecated
- parcel.writeInt(mTasks.size());
- for (int i = 0; i < mTasks.size(); i++) {
+ final int tasksSize = mTasks != null ? mTasks.size() : 0;
+ parcel.writeInt(tasksSize);
+ for (int i = 0; i < tasksSize; i++) {
mTasks.get(i).writeTaskToParcel(parcel, flags);
}
+ parcel.writeTypedList(mGroupedTasks);
parcel.writeTypedObject(mSplitBounds, flags);
parcel.writeInt(mType);
parcel.writeIntArray(mMinimizedTaskIds);
@@ -293,6 +371,16 @@ public class GroupedTaskInfo implements Parcelable {
return 0;
}
+ private String typeToString(@GroupType int type) {
+ return switch (type) {
+ case TYPE_FULLSCREEN -> "FULLSCREEN";
+ case TYPE_SPLIT -> "SPLIT";
+ case TYPE_FREEFORM -> "FREEFORM";
+ case TYPE_MIXED -> "MIXED";
+ default -> "UNKNOWN";
+ };
+ }
+
public static final Creator<GroupedTaskInfo> CREATOR = new Creator() {
@Override
public GroupedTaskInfo createFromParcel(Parcel in) {
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 06a55d3dbbd0..08079d94fcee 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
@@ -19,7 +19,6 @@
package com.android.wm.shell.apptoweb
import android.app.assist.AssistContent
-import android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
@@ -113,5 +112,5 @@ fun getDomainVerificationUserState(
* Returns the web uri from the given [AssistContent].
*/
fun AssistContent.getSessionWebUri(): Uri? {
- return extras.getParcelable(EXTRA_SESSION_TRANSFER_WEB_URI) ?: webUri
+ return sessionTransferUri ?: webUri
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index 536dc2a58534..a4620d5a4dfe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -339,7 +339,7 @@ class DesktopImmersiveController(
.setWindowCrop(leash, endBounds.width(), endBounds.height())
.apply()
onTaskResizeAnimationListener?.onAnimationEnd(taskId)
- finishCallback.onTransitionFinished(null /* wct */)
+ finishCallback.onTransitionFinished(/* wct= */ null)
}
)
addUpdateListener { animation ->
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 ceef69969d9a..e8f9a789bb98 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
@@ -306,7 +306,7 @@ class DesktopModeEventLogger {
fun logTaskInfoStateInit() {
logTaskUpdate(
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
- /* session_id */ 0,
+ sessionId = 0,
TaskUpdate(
visibleTaskCount = 0,
instanceId = 0,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index cd37113666bb..32ee319a053b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -274,7 +274,7 @@ public class DesktopModeVisualIndicator {
lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
final WindowlessWindowManager windowManager = new WindowlessWindowManager(
mTaskInfo.configuration, mLeash,
- null /* hostInputToken */);
+ /* hostInputToken= */ null);
mViewHost = new SurfaceControlViewHost(mContext,
mDisplayController.getDisplay(mTaskInfo.displayId), windowManager,
"DesktopModeVisualIndicator");
@@ -338,7 +338,7 @@ public class DesktopModeVisualIndicator {
if (mCurrentType == NO_INDICATOR) {
fadeInIndicator(newType);
} else if (newType == NO_INDICATOR) {
- fadeOutIndicator(null /* finishCallback */);
+ fadeOutIndicator(/* finishCallback= */ null);
} else {
final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType(
mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
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 d180ea7b79ff..ee817b34b24a 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
@@ -762,7 +762,7 @@ class DesktopTasksController(
return
}
val wct = WindowContainerTransaction()
- wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
+ wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true)
startLaunchTransition(
transitionType = TRANSIT_TO_FRONT,
wct = wct,
@@ -884,7 +884,7 @@ class DesktopTasksController(
} else if (Flags.enableMoveToNextDisplayShortcut()) {
applyFreeformDisplayChange(wct, task, displayId)
}
- wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
+ wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true)
if (Flags.enableDisplayFocusInShellTransitions()) {
// Bring the destination display to top with includingParents=true, so that the
// destination display gains the display focus, which makes the top task in the display
@@ -896,7 +896,7 @@ class DesktopTasksController(
performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
}
- transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+ transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
/**
@@ -1672,7 +1672,7 @@ class DesktopTasksController(
requestedTaskId,
splitPosition,
options.toBundle(),
- null, /* hideTaskToken */
+ /* hideTaskToken= */ null,
)
}
}
@@ -1709,8 +1709,8 @@ class DesktopTasksController(
fillIn,
splitPosition,
options.toBundle(),
- null /* hideTaskToken */,
- true /* forceLaunchNewTask */,
+ /* hideTaskToken= */ null,
+ /* forceLaunchNewTask= */ true,
splitIndex,
)
}
@@ -1961,7 +1961,7 @@ class DesktopTasksController(
wct.setBounds(taskInfo.token, initialBounds)
}
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
- wct.reorder(taskInfo.token, true /* onTop */)
+ wct.reorder(taskInfo.token, /* onTop= */ true)
if (useDesktopOverrideDensity()) {
wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE)
}
@@ -2796,7 +2796,7 @@ class DesktopTasksController(
controller,
"visibleTaskCount",
{ controller -> result[0] = controller.visibleTaskCount(displayId) },
- true, /* blocking */
+ /* blocking= */ true,
)
return result[0]
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 0330a5f0c4e7..c2dd4d28305b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -234,7 +234,7 @@ class DesktopTasksLimiter(
// If it's a running task, reorder it to back.
taskIdToMinimize
?.let { shellTaskOrganizer.getRunningTaskInfo(it) }
- ?.let { wct.reorder(it.token, false /* onTop */) }
+ ?.let { wct.reorder(it.token, /* onTop= */ false) }
return taskIdToMinimize
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 72c064248988..1380a9ca164f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -131,7 +131,7 @@ sealed class DragToDesktopTransitionHandler(
val pendingIntent =
PendingIntent.getActivityAsUser(
context.createContextAsUser(taskUser, /* flags= */ 0),
- 0 /* requestCode */,
+ /* requestCode= */ 0,
launchHomeIntent,
FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
options.toBundle(),
@@ -234,7 +234,7 @@ sealed class DragToDesktopTransitionHandler(
val wct = WindowContainerTransaction()
restoreWindowOrder(wct, state)
state.startTransitionFinishTransaction?.apply()
- state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
requestSplitFromScaledTask(splitPosition, wct)
clearState()
} else {
@@ -440,7 +440,7 @@ sealed class DragToDesktopTransitionHandler(
val wct = WindowContainerTransaction()
restoreWindowOrder(wct)
state.startTransitionFinishTransaction?.apply()
- state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
requestSplitSelect(wct, taskInfo, splitPosition)
}
return true
@@ -492,7 +492,7 @@ sealed class DragToDesktopTransitionHandler(
finishTransaction = startTransactionFinishT,
)
// Call finishCallback to merge animation before startTransitionFinishCb is called
- finishCallback.onTransitionFinished(null /* wct */)
+ finishCallback.onTransitionFinished(/* wct= */ null)
animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb)
} else if (isCancelTransition) {
info.changes.forEach { change ->
@@ -500,8 +500,8 @@ sealed class DragToDesktopTransitionHandler(
startTransactionFinishT.show(change.leash)
}
t.apply()
- finishCallback.onTransitionFinished(null /* wct */)
- startTransitionFinishCb.onTransitionFinished(null /* wct */)
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
clearState()
}
}
@@ -653,7 +653,7 @@ sealed class DragToDesktopTransitionHandler(
interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
} else if (state.cancelTransitionToken == transition) {
state.draggedTaskChange?.leash?.let { state.startTransitionFinishTransaction?.show(it) }
- state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
clearState()
} else {
// This transition being aborted is neither the start, nor the cancel transition, so
@@ -741,19 +741,19 @@ sealed class DragToDesktopTransitionHandler(
// TODO(b/322852244): investigate why even though these "other" tasks are
// reordered in front of home and behind the translucent dragged task, its
// surface is not visible on screen.
- wct.reorder(wc, true /* toTop */)
+ wct.reorder(wc, /* onTop= */ true)
}
val wc =
state.draggedTaskChange?.container
?: error("Dragged task should be non-null before cancelling")
// Then the dragged task a the very top.
- wct.reorder(wc, true /* toTop */)
+ wct.reorder(wc, /* onTop= */ true)
}
is TransitionState.FromSplit -> {
val wc =
state.splitRootChange?.container
?: error("Split root should be non-null before cancelling")
- wct.reorder(wc, true /* toTop */)
+ wct.reorder(wc, /* onTop= */ true)
}
}
val homeWc =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
index 964e5fd62a5f..af1679f2d175 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
@@ -36,12 +36,6 @@ import com.android.internal.os.IResultReceiver;
interface IRecentsAnimationController {
/**
- * Takes a screenshot of the task associated with the given {@param taskId}. Only valid for the
- * current set of task ids provided to the handler.
- */
- TaskSnapshot screenshotTask(int taskId);
-
- /**
* Sets the final surface transaction on a Task. This is used by Launcher to notify the system
* that animating Activity to PiP has completed and the associated task surface should be
* updated accordingly. This should be called before `finish`
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 032dac9ff3a2..76496b06a4dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -1227,19 +1227,6 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
}
@Override
- public TaskSnapshot screenshotTask(int taskId) {
- try {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId);
- return ActivityTaskManager.getService().takeTaskSnapshot(taskId,
- true /* updateCache */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to screenshot task", e);
- }
- return null;
- }
-
- @Override
public void setInputConsumerEnabled(boolean enabled) {
mExecutor.execute(() -> {
if (mFinishCB == null || !enabled) {
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 40ecdecde4e7..805f4c2fe7f8 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -67,6 +67,7 @@ import org.junit.runners.Parameterized
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2)
+@FlakyTest(bugId = 386333280)
open class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
EnterPipTransition(flicker) {
override val pipApp: PipAppHelper = PipAppHelper(instrumentation)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
index db00f41f723b..04f9ada8a9d7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
@@ -142,7 +142,7 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() {
changeMode: Int = WindowManager.TRANSIT_CLOSE,
task: RunningTaskInfo,
): TransitionInfo =
- TransitionInfo(type, 0 /* flags */).apply {
+ TransitionInfo(type, /* flags= */ 0).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
index d14c6402982d..c705f5a5ac87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
@@ -153,7 +153,7 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() {
changeMode: Int = WindowManager.TRANSIT_TO_BACK,
task: RunningTaskInfo,
): TransitionInfo =
- TransitionInfo(type, 0 /* flags */).apply {
+ TransitionInfo(type, /* flags= */ 0).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 3cf84d92a625..372e47ce6a17 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -777,7 +777,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() {
task: RunningTaskInfo,
withWallpaper: Boolean = false,
): TransitionInfo =
- TransitionInfo(WindowManager.TRANSIT_CLOSE, 0 /* flags */).apply {
+ TransitionInfo(WindowManager.TRANSIT_CLOSE, /* flags= */ 0).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
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 e032616e7d43..da27c08920dc 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
@@ -1267,7 +1267,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFullscreenTask().apply {
baseActivity = baseComponent
@@ -1284,7 +1284,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFullscreenTask().apply {
baseActivity = baseComponent
@@ -1757,12 +1757,12 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val wallpaperChange =
- hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }
- assertThat(wallpaperChange).isNotNull()
- assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
- }
+ val wallpaperChange =
+ getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { op ->
+ op.container == wallpaperToken.asBinder()
+ }
+ assertNotNull(wallpaperChange)
+ assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
}
@Test
@@ -1792,15 +1792,13 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
- // to the right margin and the ratio of the top margin to bottom margin are also
- // preserved.
- assertThat(taskChange!!.configuration.windowConfiguration.bounds)
- .isEqualTo(Rect(240, 160, 560, 400))
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
+ // to the right margin and the ratio of the top margin to bottom margin are also
+ // preserved.
+ assertThat(taskChange.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(240, 160, 560, 400))
}
@Test
@@ -1831,12 +1829,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- assertThat(taskChange!!.configuration.windowConfiguration.bounds)
- .isEqualTo(Rect(960, 480, 1280, 720))
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ assertThat(taskChange.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(960, 480, 1280, 720))
}
@Test
@@ -1864,13 +1860,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- // DP size is preserved. The window is centered in the destination display.
- assertThat(taskChange!!.configuration.windowConfiguration.bounds)
- .isEqualTo(Rect(320, 120, 960, 600))
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ // DP size is preserved. The window is centered in the destination display.
+ assertThat(taskChange.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(320, 120, 960, 600))
}
@Test
@@ -1903,14 +1897,12 @@ class DesktopTasksControllerTest : ShellTestCase() {
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- assertThat(taskChange!!.configuration.windowConfiguration.bounds.left).isAtLeast(0)
- assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
- assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
- assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.left).isAtLeast(0)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
}
@Test
@@ -2722,7 +2714,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFreeformTask().apply {
baseActivity = baseComponent
@@ -2743,7 +2735,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFullscreenTask().apply {
baseActivity = baseComponent
@@ -3376,11 +3368,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, -100), /* position */
- PointF(200f, -200f), /* inputCoordinate */
- Rect(100, -100, 500, 1000), /* currentDragBounds */
- Rect(0, 50, 2000, 2000), /* validDragArea */
- Rect() /* dragStartBounds */,
+ position = Point(100, -100),
+ inputCoordinate = PointF(200f, -200f),
+ currentDragBounds = Rect(100, -100, 500, 1000),
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3415,11 +3407,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 200), /* position */
- PointF(200f, 300f), /* inputCoordinate */
- currentDragBounds, /* currentDragBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ position = Point(100, 200),
+ inputCoordinate = PointF(200f, 300f),
+ currentDragBounds = currentDragBounds,
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3459,11 +3451,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 50), /* position */
- PointF(200f, 300f), /* inputCoordinate */
- Rect(100, 50, 500, 1000), /* currentDragBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ position = Point(100, 50),
+ inputCoordinate = PointF(200f, 300f),
+ currentDragBounds = Rect(100, 50, 500, 1000),
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3498,11 +3490,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 50), /* position */
- PointF(200f, 300f), /* inputCoordinate */
+ position = Point(100, 50),
+ inputCoordinate = PointF(200f, 300f),
currentDragBounds,
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3555,11 +3547,11 @@ class DesktopTasksControllerTest : ShellTestCase() {
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 50), /* position */
- PointF(200f, 300f), /* inputCoordinate */
- currentDragBounds, /* currentDragBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ position = Point(100, 50),
+ inputCoordinate = PointF(200f, 300f),
+ currentDragBounds = currentDragBounds,
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -5053,7 +5045,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
task: RunningTaskInfo?,
@WindowManager.TransitionType type: Int = TRANSIT_OPEN,
): TransitionRequestInfo {
- return TransitionRequestInfo(type, task, null /* remoteTransition */)
+ return TransitionRequestInfo(type, task, /* remoteTransition= */ null)
}
private companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 52602f22fd4b..c8214b3838e2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -193,10 +193,10 @@ class DesktopTasksLimiterTest : ShellTestCase() {
desktopTasksLimiter
.getTransitionObserver()
.onTransitionReady(
- Binder() /* transition */,
+ /* transition= */ Binder(),
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -217,10 +217,10 @@ class DesktopTasksLimiterTest : ShellTestCase() {
desktopTasksLimiter
.getTransitionObserver()
.onTransitionReady(
- taskTransition /* transition */,
+ /* transition= */ taskTransition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -242,8 +242,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -265,8 +265,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -287,8 +287,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -316,8 +316,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
transition,
TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) },
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -344,8 +344,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
newTransition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -552,8 +552,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
@@ -584,8 +584,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
@@ -617,8 +617,8 @@ class DesktopTasksLimiterTest : ShellTestCase() {
.onTransitionReady(
mergedTransition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
desktopTasksLimiter.getTransitionObserver().onTransitionStarting(mergedTransition)
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 d491d445458d..3cc30cb491b3 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
@@ -305,7 +305,7 @@ class DesktopTasksTransitionObserverTest {
type: Int = TRANSIT_TO_BACK,
withWallpaper: Boolean = false,
): TransitionInfo {
- return TransitionInfo(type, 0 /* flags */).apply {
+ return TransitionInfo(type, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = type
@@ -331,7 +331,7 @@ class DesktopTasksTransitionObserverTest {
task: RunningTaskInfo?,
type: Int = TRANSIT_OPEN,
): TransitionInfo {
- return TransitionInfo(TRANSIT_OPEN, 0 /* flags */).apply {
+ return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_OPEN
@@ -344,7 +344,7 @@ class DesktopTasksTransitionObserverTest {
}
private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo {
- return TransitionInfo(TRANSIT_CLOSE, 0 /* flags */).apply {
+ return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_CLOSE
@@ -357,7 +357,7 @@ class DesktopTasksTransitionObserverTest {
}
private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo {
- return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply {
+ return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_TO_BACK
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 2216d5452ce5..341df0299a97 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -677,7 +677,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo {
- return TransitionInfo(type, 0 /* flags */).apply {
+ return TransitionInfo(type, /* flags= */ 0).apply {
addChange( // Home.
TransitionInfo.Change(mock(), homeTaskLeash).apply {
parent = null
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index fd3adabfd44b..32096645aea7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -40,6 +40,7 @@ import org.mockito.Mockito.mock
/**
* Tests for [GroupedTaskInfo]
+ * Build & Run: atest WMShellUnitTests:GroupedTaskInfoTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -47,7 +48,7 @@ class GroupedTaskInfoTest : ShellTestCase() {
@Test
fun testSingleTask_hasCorrectType() {
- assertThat(singleTaskGroupInfo().type).isEqualTo(TYPE_FULLSCREEN)
+ assertThat(singleTaskGroupInfo().isBaseType(TYPE_FULLSCREEN)).isTrue()
}
@Test
@@ -66,7 +67,7 @@ class GroupedTaskInfoTest : ShellTestCase() {
@Test
fun testSplitTasks_hasCorrectType() {
- assertThat(splitTasksGroupInfo().type).isEqualTo(TYPE_SPLIT)
+ assertThat(splitTasksGroupInfo().isBaseType(TYPE_SPLIT)).isTrue()
}
@Test
@@ -87,8 +88,8 @@ class GroupedTaskInfoTest : ShellTestCase() {
@Test
fun testFreeformTasks_hasCorrectType() {
- assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).type)
- .isEqualTo(TYPE_FREEFORM)
+ assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).isBaseType(TYPE_FREEFORM))
+ .isTrue()
}
@Test
@@ -111,83 +112,155 @@ class GroupedTaskInfoTest : ShellTestCase() {
}
@Test
+ fun testMixedWithFullscreenBase_hasCorrectType() {
+ assertThat(mixedTaskGroupInfoWithFullscreenBase().isBaseType(TYPE_FULLSCREEN)).isTrue()
+ }
+
+ @Test
+ fun testMixedWithSplitBase_hasCorrectType() {
+ assertThat(mixedTaskGroupInfoWithSplitBase().isBaseType(TYPE_SPLIT)).isTrue()
+ }
+
+ @Test
+ fun testMixedWithFreeformBase_hasCorrectType() {
+ assertThat(mixedTaskGroupInfoWithFreeformBase().isBaseType(TYPE_FREEFORM)).isTrue()
+ }
+
+ @Test
+ fun testMixed_disallowEmptyMixed() {
+ assertThrows(IllegalArgumentException::class.java) {
+ GroupedTaskInfo.forMixed(listOf())
+ }
+ }
+
+ @Test
+ fun testMixed_disallowNestedMixed() {
+ assertThrows(IllegalArgumentException::class.java) {
+ GroupedTaskInfo.forMixed(listOf(
+ GroupedTaskInfo.forMixed(listOf(singleTaskGroupInfo()))))
+ }
+ }
+
+ @Test
+ fun testMixed_disallowNonMixedAccessors() {
+ val mixed = mixedTaskGroupInfoWithFullscreenBase()
+ assertThrows(IllegalStateException::class.java) {
+ mixed.taskInfo1
+ }
+ assertThrows(IllegalStateException::class.java) {
+ mixed.taskInfo2
+ }
+ assertThrows(IllegalStateException::class.java) {
+ mixed.splitBounds
+ }
+ assertThrows(IllegalStateException::class.java) {
+ mixed.minimizedTaskIds
+ }
+ }
+
+ @Test
fun testParcelling_singleTask() {
- val recentTaskInfo = singleTaskGroupInfo()
+ val taskInfo = singleTaskGroupInfo()
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FULLSCREEN)
- assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
- assertThat(recentTaskInfoParcel.taskInfo2).isNull()
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FULLSCREEN)).isTrue()
+ assertThat(taskInfoFromParcel.taskInfo1.taskId).isEqualTo(1)
+ assertThat(taskInfoFromParcel.taskInfo2).isNull()
}
@Test
fun testParcelling_splitTasks() {
- val recentTaskInfo = splitTasksGroupInfo()
+ val taskInfo = splitTasksGroupInfo()
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_SPLIT)
- assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
- assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
- assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
- assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
- assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_SPLIT)).isTrue()
+ assertThat(taskInfoFromParcel.taskInfo1.taskId).isEqualTo(1)
+ assertThat(taskInfoFromParcel.taskInfo2).isNotNull()
+ assertThat(taskInfoFromParcel.taskInfo2!!.taskId).isEqualTo(2)
+ assertThat(taskInfoFromParcel.splitBounds).isNotNull()
+ assertThat(taskInfoFromParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
}
@Test
fun testParcelling_freeformTasks() {
- val recentTaskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3))
+ val taskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3))
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
- assertThat(recentTaskInfoParcel.taskInfoList).hasSize(3)
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+ assertThat(taskInfoFromParcel.taskInfoList).hasSize(3)
// Only compare task ids
val taskIdComparator = Correspondence.transforming<TaskInfo, Int>(
{ it?.taskId }, "has taskId of"
)
- assertThat(recentTaskInfoParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
- .containsExactly(1, 2, 3)
+ assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
+ .containsExactly(1, 2, 3).inOrder()
}
@Test
fun testParcelling_freeformTasks_minimizedTasks() {
- val recentTaskInfo = freeformTasksGroupInfo(
+ val taskInfo = freeformTasksGroupInfo(
freeformTaskIds = arrayOf(1, 2, 3), minimizedTaskIds = arrayOf(2))
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
- assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+ assertThat(taskInfoFromParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
}
@Test
- fun testGetTaskById_singleTasks() {
+ fun testParcelling_mixedTasks() {
+ val taskInfo = GroupedTaskInfo.forMixed(listOf(
+ freeformTasksGroupInfo(freeformTaskIds = arrayOf(4, 5, 6),
+ minimizedTaskIds = arrayOf(5)),
+ splitTasksGroupInfo(firstId = 2, secondId = 3),
+ singleTaskGroupInfo(id = 1)))
+
+ val parcel = Parcel.obtain()
+ taskInfo.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+
+ // Read the object back from the parcel
+ val taskInfoFromParcel: GroupedTaskInfo =
+ GroupedTaskInfo.CREATOR.createFromParcel(parcel)
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+ assertThat(taskInfoFromParcel.baseGroupedTask.minimizedTaskIds).isEqualTo(
+ arrayOf(5).toIntArray())
+ for (i in 1..6) {
+ assertThat(taskInfoFromParcel.containsTask(i)).isTrue()
+ }
+ assertThat(taskInfoFromParcel.taskInfoList).hasSize(taskInfo.taskInfoList.size)
+ }
+
+ @Test
+ fun testTaskProperties_singleTasks() {
val task1 = createTaskInfo(id = 1234)
val taskInfo = GroupedTaskInfo.forFullscreenTasks(task1)
assertThat(taskInfo.getTaskById(1234)).isEqualTo(task1)
assertThat(taskInfo.containsTask(1234)).isTrue()
+ assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1))
}
@Test
- fun testGetTaskById_multipleTasks() {
+ fun testTaskProperties_splitTasks() {
val task1 = createTaskInfo(id = 1)
val task2 = createTaskInfo(id = 2)
val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
@@ -198,6 +271,41 @@ class GroupedTaskInfoTest : ShellTestCase() {
assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
assertThat(taskInfo.containsTask(1)).isTrue()
assertThat(taskInfo.containsTask(2)).isTrue()
+ assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1, task2))
+ }
+
+ @Test
+ fun testTaskProperties_freeformTasks() {
+ val task1 = createTaskInfo(id = 1)
+ val task2 = createTaskInfo(id = 2)
+
+ val taskInfo = GroupedTaskInfo.forFreeformTasks(listOf(task1, task2), setOf())
+
+ assertThat(taskInfo.getTaskById(1)).isEqualTo(task1)
+ assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
+ assertThat(taskInfo.containsTask(1)).isTrue()
+ assertThat(taskInfo.containsTask(2)).isTrue()
+ assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1, task2))
+ }
+
+ @Test
+ fun testTaskProperties_mixedTasks() {
+ val task1 = createTaskInfo(id = 1)
+ val task2 = createTaskInfo(id = 2)
+ val task3 = createTaskInfo(id = 3)
+ val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
+
+ val splitTasks = GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
+ val fullscreenTasks = GroupedTaskInfo.forFullscreenTasks(task3)
+ val mixedTasks = GroupedTaskInfo.forMixed(listOf(splitTasks, fullscreenTasks))
+
+ assertThat(mixedTasks.getTaskById(1)).isEqualTo(task1)
+ assertThat(mixedTasks.getTaskById(2)).isEqualTo(task2)
+ assertThat(mixedTasks.getTaskById(3)).isEqualTo(task3)
+ assertThat(mixedTasks.containsTask(1)).isTrue()
+ assertThat(mixedTasks.containsTask(2)).isTrue()
+ assertThat(mixedTasks.containsTask(3)).isTrue()
+ assertThat(mixedTasks.taskInfoList).isEqualTo(listOf(task1, task2, task3))
}
private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply {
@@ -205,14 +313,14 @@ class GroupedTaskInfoTest : ShellTestCase() {
token = WindowContainerToken(mock(IWindowContainerToken::class.java))
}
- private fun singleTaskGroupInfo(): GroupedTaskInfo {
- val task = createTaskInfo(id = 1)
+ private fun singleTaskGroupInfo(id: Int = 1): GroupedTaskInfo {
+ val task = createTaskInfo(id)
return GroupedTaskInfo.forFullscreenTasks(task)
}
- private fun splitTasksGroupInfo(): GroupedTaskInfo {
- val task1 = createTaskInfo(id = 1)
- val task2 = createTaskInfo(id = 2)
+ private fun splitTasksGroupInfo(firstId: Int = 1, secondId: Int = 2): GroupedTaskInfo {
+ val task1 = createTaskInfo(firstId)
+ val task2 = createTaskInfo(secondId)
val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
return GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
}
@@ -225,4 +333,22 @@ class GroupedTaskInfoTest : ShellTestCase() {
freeformTaskIds.map { createTaskInfo(it) }.toList(),
minimizedTaskIds.toSet())
}
+
+ private fun mixedTaskGroupInfoWithFullscreenBase(): GroupedTaskInfo {
+ return GroupedTaskInfo.forMixed(listOf(
+ singleTaskGroupInfo(id = 1),
+ singleTaskGroupInfo(id = 2)))
+ }
+
+ private fun mixedTaskGroupInfoWithSplitBase(): GroupedTaskInfo {
+ return GroupedTaskInfo.forMixed(listOf(
+ splitTasksGroupInfo(firstId = 2, secondId = 3),
+ singleTaskGroupInfo(id = 1)))
+ }
+
+ private fun mixedTaskGroupInfoWithFreeformBase(): GroupedTaskInfo {
+ return GroupedTaskInfo.forMixed(listOf(
+ freeformTasksGroupInfo(freeformTaskIds = arrayOf(2, 3, 4)),
+ singleTaskGroupInfo(id = 1)))
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 22b45e8c63af..7e5d6ce38c5a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -24,6 +24,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.launcher3.Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FULLSCREEN;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static org.junit.Assert.assertEquals;
@@ -346,9 +349,9 @@ public class RecentTasksControllerTest extends ShellTestCase {
GroupedTaskInfo singleGroup2 = recentTasks.get(2);
// Check that groups have expected types
- assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup1.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup2.getType());
+ assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+ assertTrue(singleGroup1.isBaseType(TYPE_FULLSCREEN));
+ assertTrue(singleGroup2.isBaseType(TYPE_FULLSCREEN));
// Check freeform group entries
assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
@@ -385,9 +388,9 @@ public class RecentTasksControllerTest extends ShellTestCase {
GroupedTaskInfo singleGroup = recentTasks.get(2);
// Check that groups have expected types
- assertEquals(GroupedTaskInfo.TYPE_SPLIT, splitGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup.getType());
+ assertTrue(splitGroup.isBaseType(TYPE_SPLIT));
+ assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+ assertTrue(singleGroup.isBaseType(TYPE_FULLSCREEN));
// Check freeform group entries
assertEquals(t3, freeformGroup.getTaskInfoList().get(0));
@@ -420,10 +423,10 @@ public class RecentTasksControllerTest extends ShellTestCase {
// Expect no grouping of tasks
assertEquals(4, recentTasks.size());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(0).getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(1).getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(2).getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(3).getType());
+ assertTrue(recentTasks.get(0).isBaseType(TYPE_FULLSCREEN));
+ assertTrue(recentTasks.get(1).isBaseType(TYPE_FULLSCREEN));
+ assertTrue(recentTasks.get(2).isBaseType(TYPE_FULLSCREEN));
+ assertTrue(recentTasks.get(3).isBaseType(TYPE_FULLSCREEN));
assertEquals(t1, recentTasks.get(0).getTaskInfo1());
assertEquals(t2, recentTasks.get(1).getTaskInfo1());
@@ -457,9 +460,9 @@ public class RecentTasksControllerTest extends ShellTestCase {
GroupedTaskInfo singleGroup2 = recentTasks.get(2);
// Check that groups have expected types
- assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup1.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup2.getType());
+ assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+ assertTrue(singleGroup1.isBaseType(TYPE_FULLSCREEN));
+ assertTrue(singleGroup2.isBaseType(TYPE_FULLSCREEN));
// Check freeform group entries
assertEquals(3, freeformGroup.getTaskInfoList().size());
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 7dac0859b7e9..6b02aeffd42a 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
@@ -20,7 +20,6 @@ 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_UNDEFINED;
-import static android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
@@ -1176,7 +1175,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
- public void webUriLink_webUriLinkUsedWhenWhenAvailable() {
+ public void sessionTransferUri_sessionTransferUriUsedWhenWhenAvailable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
@@ -1188,7 +1187,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
- public void webUriLink_webUriLinkUsedWhenSessionTransferUriUnavailable() {
+ public void webUri_webUriUsedWhenSessionTransferUriUnavailable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
@@ -1200,7 +1199,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
- public void genericLink_genericLinkUsedWhenCapturedLinkAndWebUriUnavailable() {
+ public void genericLink_genericLinkUsedWhenCapturedLinkAndAssistContentUriUnavailable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, null /* captured link */, null /* web uri */,
@@ -1490,7 +1489,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
taskInfo.capturedLink = capturedLink;
taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
mAssistContent.setWebUri(webUri);
- mAssistContent.getExtras().putObject(EXTRA_SESSION_TRANSFER_WEB_URI, sessionTransferUri);
+ mAssistContent.setSessionTransferUri(sessionTransferUri);
final String genericLinkString = genericLink == null ? null : genericLink.toString();
doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any());
// Relayout to set captured link
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 36f62da651db..c9625c405faa 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -5866,19 +5866,31 @@ final public class MediaCodec {
@NonNull MediaCodec codec, @NonNull MediaFormat format);
/**
- * Called when the metrics for this codec have been flushed due to the
- * start of a new subsession.
+ * Called when the metrics for this codec have been flushed "mid-stream"
+ * due to the start of a new subsession during execution.
* <p>
- * This can happen when the codec is reconfigured after stop(), or
- * mid-stream e.g. if the video size changes. When this happens, the
- * metrics for the previous subsession are flushed, and
- * {@link MediaCodec#getMetrics} will return the metrics for the
- * new subsession. This happens just before the {@link Callback#onOutputFormatChanged}
+ * A new codec subsession normally starts when the codec is reconfigured
+ * after stop(), but it can also happen mid-stream e.g. if the video size
+ * changes. When this happens, the metrics for the previous subsession
+ * are flushed, and {@link MediaCodec#getMetrics} will return the metrics
+ * for the new subsession.
+ * <p>
+ * For subsessions that begin due to a reconfiguration, the metrics for
+ * the prior subsession can be retrieved via {@link MediaCodec#getMetrics}
+ * prior to calling {@link #configure}.
+ * <p>
+ * When a new subsession begins "mid-stream", the metrics for the prior
+ * subsession are flushed just before the {@link Callback#onOutputFormatChanged}
* event, so this <b>optional</b> callback is provided to be able to
* capture the final metrics for the previous subsession.
*
* @param codec The MediaCodec object.
- * @param metrics The flushed metrics for this codec.
+ * @param metrics The flushed metrics for this codec. This is a
+ * {@link PersistableBundle} containing the set of
+ * attributes and values available for the media being
+ * handled by this instance of MediaCodec. The attributes
+ * are described in {@link MetricsConstants}. Additional
+ * vendor-specific fields may also be present.
*/
@FlaggedApi(FLAG_SUBSESSION_METRICS)
public void onMetricsFlushed(
diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java
index 678150b9f3a1..4c5efc1cf24b 100644
--- a/media/java/android/media/MediaMuxer.java
+++ b/media/java/android/media/MediaMuxer.java
@@ -18,6 +18,8 @@ package android.media;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.media.MediaCodec.BufferInfo;
import android.os.Build;
@@ -257,6 +259,8 @@ final public class MediaMuxer {
*/
private OutputFormat() {}
/** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
public static final int MUXER_OUTPUT_FIRST = 0;
/** MPEG4 media file format*/
public static final int MUXER_OUTPUT_MPEG_4 = MUXER_OUTPUT_FIRST;
@@ -269,6 +273,8 @@ final public class MediaMuxer {
/** Ogg media file format*/
public static final int MUXER_OUTPUT_OGG = MUXER_OUTPUT_FIRST + 4;
/** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
public static final int MUXER_OUTPUT_LAST = MUXER_OUTPUT_OGG;
};
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index f629c8802aef..b30b779b57b5 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -320,6 +320,9 @@ LIBANDROID {
ASystemFontIterator_open; # introduced=29
ASystemFontIterator_close; # introduced=29
ASystemFontIterator_next; # introduced=29
+ ASystemHealth_getMaxCpuHeadroomTidsSize; # introduced=36
+ ASystemHealth_getCpuHeadroomCalculationWindowRange; # introduced=36
+ ASystemHealth_getGpuHeadroomCalculationWindowRange; # introduced=36
ASystemHealth_getCpuHeadroom; # introduced=36
ASystemHealth_getGpuHeadroom; # introduced=36
ASystemHealth_getCpuHeadroomMinIntervalMillis; # introduced=36
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 68c1983825a2..1e6a7b7f2810 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -859,7 +859,7 @@ void APerformanceHintManager::layersFromNativeSurfaces(ANativeWindow** windows,
std::vector<ANativeWindow*> windowVec(windows, windows + numWindows);
for (auto&& window : windowVec) {
Surface* surface = static_cast<Surface*>(window);
- if (Surface::isValid(surface)) {
+ if (surface != nullptr) {
const sp<IBinder>& handle = surface->getSurfaceControlHandle();
if (handle != nullptr) {
out.push_back(handle);
diff --git a/native/android/system_health.cpp b/native/android/system_health.cpp
index f3fa9f6836d5..5c07ac7bfccc 100644
--- a/native/android/system_health.cpp
+++ b/native/android/system_health.cpp
@@ -31,26 +31,28 @@ namespace hal = aidl::android::hardware::power;
struct ACpuHeadroomParams : public CpuHeadroomParamsInternal {};
struct AGpuHeadroomParams : public GpuHeadroomParamsInternal {};
-const int CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
-const int CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
-const int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
-const int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
-const int CPU_HEADROOM_MAX_TID_COUNT = 5;
-
struct ASystemHealthManager {
public:
static ASystemHealthManager* getInstance();
- ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager);
+
+ ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager,
+ IHintManager::HintManagerClientData&& clientData);
ASystemHealthManager() = delete;
~ASystemHealthManager();
int getCpuHeadroom(const ACpuHeadroomParams* params, float* outHeadroom);
int getGpuHeadroom(const AGpuHeadroomParams* params, float* outHeadroom);
int getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis);
int getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis);
+ int getMaxCpuHeadroomTidsSize(size_t* outSize);
+ int getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis);
+ int getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis);
private:
static ASystemHealthManager* create(std::shared_ptr<IHintManager> hintManager);
std::shared_ptr<IHintManager> mHintManager;
+ IHintManager::HintManagerClientData mClientData;
};
ASystemHealthManager* ASystemHealthManager::getInstance() {
@@ -60,10 +62,11 @@ ASystemHealthManager* ASystemHealthManager::getInstance() {
return instance;
}
-ASystemHealthManager::ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager)
- : mHintManager(std::move(hintManager)) {}
+ASystemHealthManager::ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager,
+ IHintManager::HintManagerClientData&& clientData)
+ : mHintManager(std::move(hintManager)), mClientData(clientData) {}
-ASystemHealthManager::~ASystemHealthManager() {}
+ASystemHealthManager::~ASystemHealthManager() = default;
ASystemHealthManager* ASystemHealthManager::create(std::shared_ptr<IHintManager> hintManager) {
if (!hintManager) {
@@ -74,20 +77,37 @@ ASystemHealthManager* ASystemHealthManager::create(std::shared_ptr<IHintManager>
ALOGE("%s: PerformanceHint service is not ready ", __FUNCTION__);
return nullptr;
}
- return new ASystemHealthManager(hintManager);
-}
-
-ASystemHealthManager* ASystemHealth_acquireManager() {
- return ASystemHealthManager::getInstance();
+ IHintManager::HintManagerClientData clientData;
+ ndk::ScopedAStatus ret = hintManager->getClientData(&clientData);
+ if (!ret.isOk()) {
+ ALOGE("%s: PerformanceHint service is not initialized %s", __FUNCTION__, ret.getMessage());
+ return nullptr;
+ }
+ return new ASystemHealthManager(hintManager, std::move(clientData));
}
int ASystemHealthManager::getCpuHeadroom(const ACpuHeadroomParams* params, float* outHeadroom) {
+ if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
std::optional<hal::CpuHeadroomResult> res;
::ndk::ScopedAStatus ret;
CpuHeadroomParamsInternal internalParams;
if (!params) {
ret = mHintManager->getCpuHeadroom(internalParams, &res);
} else {
+ LOG_ALWAYS_FATAL_IF((int)params->tids.size() > mClientData.maxCpuHeadroomThreads,
+ "%s: tids size should not exceed %d", __FUNCTION__,
+ mClientData.maxCpuHeadroomThreads);
+ LOG_ALWAYS_FATAL_IF(params->calculationWindowMillis <
+ mClientData.supportInfo.headroom
+ .cpuMinCalculationWindowMillis ||
+ params->calculationWindowMillis >
+ mClientData.supportInfo.headroom
+ .cpuMaxCalculationWindowMillis,
+ "%s: calculationWindowMillis should be in range [%d, %d] but got %d",
+ __FUNCTION__,
+ mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis,
+ mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis,
+ params->calculationWindowMillis);
ret = mHintManager->getCpuHeadroom(*params, &res);
}
if (!ret.isOk()) {
@@ -106,12 +126,24 @@ int ASystemHealthManager::getCpuHeadroom(const ACpuHeadroomParams* params, float
}
int ASystemHealthManager::getGpuHeadroom(const AGpuHeadroomParams* params, float* outHeadroom) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
std::optional<hal::GpuHeadroomResult> res;
::ndk::ScopedAStatus ret;
GpuHeadroomParamsInternal internalParams;
if (!params) {
ret = mHintManager->getGpuHeadroom(internalParams, &res);
} else {
+ LOG_ALWAYS_FATAL_IF(params->calculationWindowMillis <
+ mClientData.supportInfo.headroom
+ .gpuMinCalculationWindowMillis ||
+ params->calculationWindowMillis >
+ mClientData.supportInfo.headroom
+ .gpuMaxCalculationWindowMillis,
+ "%s: calculationWindowMillis should be in range [%d, %d] but got %d",
+ __FUNCTION__,
+ mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis,
+ mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis,
+ params->calculationWindowMillis);
ret = mHintManager->getGpuHeadroom(*params, &res);
}
if (!ret.isOk()) {
@@ -128,6 +160,7 @@ int ASystemHealthManager::getGpuHeadroom(const AGpuHeadroomParams* params, float
}
int ASystemHealthManager::getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) {
+ if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
int64_t minIntervalMillis = 0;
::ndk::ScopedAStatus ret = mHintManager->getCpuHeadroomMinIntervalMillis(&minIntervalMillis);
if (!ret.isOk()) {
@@ -142,6 +175,7 @@ int ASystemHealthManager::getCpuHeadroomMinIntervalMillis(int64_t* outMinInterva
}
int ASystemHealthManager::getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
int64_t minIntervalMillis = 0;
::ndk::ScopedAStatus ret = mHintManager->getGpuHeadroomMinIntervalMillis(&minIntervalMillis);
if (!ret.isOk()) {
@@ -155,6 +189,57 @@ int ASystemHealthManager::getGpuHeadroomMinIntervalMillis(int64_t* outMinInterva
return OK;
}
+int ASystemHealthManager::getMaxCpuHeadroomTidsSize(size_t* outSize) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
+ *outSize = mClientData.maxCpuHeadroomThreads;
+ return OK;
+}
+
+int ASystemHealthManager::getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
+ *outMinMillis = mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis;
+ *outMaxMillis = mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis;
+ return OK;
+}
+
+int ASystemHealthManager::getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
+ *outMinMillis = mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis;
+ *outMaxMillis = mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis;
+ return OK;
+}
+
+int ASystemHealth_getMaxCpuHeadroomTidsSize(size_t* _Nonnull outSize) {
+ LOG_ALWAYS_FATAL_IF(outSize == nullptr, "%s: outSize should not be null", __FUNCTION__);
+ auto manager = ASystemHealthManager::getInstance();
+ if (manager == nullptr) return ENOTSUP;
+ return manager->getMaxCpuHeadroomTidsSize(outSize);
+}
+
+int ASystemHealth_getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ LOG_ALWAYS_FATAL_IF(outMinMillis == nullptr, "%s: outMinMillis should not be null",
+ __FUNCTION__);
+ LOG_ALWAYS_FATAL_IF(outMaxMillis == nullptr, "%s: outMaxMillis should not be null",
+ __FUNCTION__);
+ auto manager = ASystemHealthManager::getInstance();
+ if (manager == nullptr) return ENOTSUP;
+ return manager->getCpuHeadroomCalculationWindowRange(outMinMillis, outMaxMillis);
+}
+
+int ASystemHealth_getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ LOG_ALWAYS_FATAL_IF(outMinMillis == nullptr, "%s: outMinMillis should not be null",
+ __FUNCTION__);
+ LOG_ALWAYS_FATAL_IF(outMaxMillis == nullptr, "%s: outMaxMillis should not be null",
+ __FUNCTION__);
+ auto manager = ASystemHealthManager::getInstance();
+ if (manager == nullptr) return ENOTSUP;
+ return manager->getGpuHeadroomCalculationWindowRange(outMinMillis, outMaxMillis);
+}
+
int ASystemHealth_getCpuHeadroom(const ACpuHeadroomParams* _Nullable params,
float* _Nonnull outHeadroom) {
LOG_ALWAYS_FATAL_IF(outHeadroom == nullptr, "%s: outHeadroom should not be null", __FUNCTION__);
@@ -189,19 +274,15 @@ int ASystemHealth_getGpuHeadroomMinIntervalMillis(int64_t* _Nonnull outMinInterv
void ACpuHeadroomParams_setCalculationWindowMillis(ACpuHeadroomParams* _Nonnull params,
int windowMillis) {
- LOG_ALWAYS_FATAL_IF(windowMillis < CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN ||
- windowMillis > CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX,
- "%s: windowMillis should be in range [50, 10000] but got %d", __FUNCTION__,
- windowMillis);
+ LOG_ALWAYS_FATAL_IF(windowMillis <= 0, "%s: windowMillis should be positive but got %d",
+ __FUNCTION__, windowMillis);
params->calculationWindowMillis = windowMillis;
}
void AGpuHeadroomParams_setCalculationWindowMillis(AGpuHeadroomParams* _Nonnull params,
int windowMillis) {
- LOG_ALWAYS_FATAL_IF(windowMillis < GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN ||
- windowMillis > GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX,
- "%s: windowMillis should be in range [50, 10000] but got %d", __FUNCTION__,
- windowMillis);
+ LOG_ALWAYS_FATAL_IF(windowMillis <= 0, "%s: windowMillis should be positive but got %d",
+ __FUNCTION__, windowMillis);
params->calculationWindowMillis = windowMillis;
}
@@ -214,13 +295,11 @@ int AGpuHeadroomParams_getCalculationWindowMillis(AGpuHeadroomParams* _Nonnull p
}
void ACpuHeadroomParams_setTids(ACpuHeadroomParams* _Nonnull params, const int* _Nonnull tids,
- int tidsSize) {
+ size_t tidsSize) {
LOG_ALWAYS_FATAL_IF(tids == nullptr, "%s: tids should not be null", __FUNCTION__);
- LOG_ALWAYS_FATAL_IF(tidsSize > CPU_HEADROOM_MAX_TID_COUNT, "%s: tids size should not exceed 5",
- __FUNCTION__);
params->tids.resize(tidsSize);
params->tids.clear();
- for (int i = 0; i < tidsSize; ++i) {
+ for (int i = 0; i < (int)tidsSize; ++i) {
LOG_ALWAYS_FATAL_IF(tids[i] <= 0, "ACpuHeadroomParams_setTids: Invalid non-positive tid %d",
tids[i]);
params->tids[i] = tids[i];
@@ -269,10 +348,10 @@ AGpuHeadroomParams* _Nonnull AGpuHeadroomParams_create() {
return new AGpuHeadroomParams();
}
-void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nonnull params) {
+void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nullable params) {
delete params;
}
-void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nonnull params) {
+void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nullable params) {
delete params;
}
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index f68fa1a89540..0fa92eab4c0a 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -161,6 +161,9 @@ public:
clientDataIn,
::aidl::android::os::IHintManager::HintManagerClientData* _aidl_return),
(override));
+ MOCK_METHOD(ScopedAStatus, getClientData,
+ (::aidl::android::os::IHintManager::HintManagerClientData * _aidl_return),
+ (override));
MOCK_METHOD(SpAIBinder, asBinder, (), (override));
MOCK_METHOD(bool, isRemote, (), (override));
};
@@ -602,6 +605,15 @@ TEST_F(PerformanceHintTest, TestASessionCreationConfig) {
ASSERT_NE(config, nullptr);
}
+TEST_F(PerformanceHintTest, TestSessionCreationWithNullLayers) {
+ EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _)).Times(1);
+ auto&& config = configFromCreator(
+ {.tids = mTids, .nativeWindows = {nullptr}, .surfaceControls = {nullptr}});
+ APerformanceHintManager* manager = createManager();
+ auto&& session = createSessionUsingConfig(manager, config);
+ ASSERT_TRUE(session);
+}
+
TEST_F(PerformanceHintTest, TestSupportObject) {
// Disable GPU and Power Efficiency support to test partial enabling
mClientData.supportInfo.sessionModes &= ~(1 << (int)hal::SessionMode::AUTO_GPU);
diff --git a/packages/CredentialManager/tests/robotests/Android.bp b/packages/CredentialManager/tests/robotests/Android.bp
index 27afaaa49fdd..01f403d3719d 100644
--- a/packages/CredentialManager/tests/robotests/Android.bp
+++ b/packages/CredentialManager/tests/robotests/Android.bp
@@ -53,7 +53,6 @@ android_robolectric_test {
"android.test.mock.stubs.system",
"truth",
],
- upstream: true,
java_resource_dirs: ["config"],
instrumentation_for: "CredentialManagerRobo",
}
diff --git a/packages/CredentialManager/wear/robotests/Android.bp b/packages/CredentialManager/wear/robotests/Android.bp
index 589a3d6cc103..db3c36355dde 100644
--- a/packages/CredentialManager/wear/robotests/Android.bp
+++ b/packages/CredentialManager/wear/robotests/Android.bp
@@ -24,6 +24,5 @@ android_robolectric_test {
"framework_graphics_flags_java_lib",
],
java_resource_dirs: ["config"],
- upstream: true,
strict_mode: false,
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
new file mode 100644
index 000000000000..b384a2418ff2
--- /dev/null
+++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
@@ -0,0 +1,357 @@
+# 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.
+
+#
+# Romanian keyboard layout.
+#
+
+type OVERLAY
+
+map key 86 PLUS
+
+### ROW 1
+
+key GRAVE {
+ label: '\u201e'
+ base: '\u201e'
+ shift: '\u201d'
+ ralt: '`'
+ ralt+shift: '~'
+}
+
+key 1 {
+ label: '1'
+ base: '1'
+ shift: '!'
+ ralt: '\u0303'
+}
+
+key 2 {
+ label: '2'
+ base: '2'
+ shift: '@'
+ ralt: '\u030C'
+}
+
+key 3 {
+ label: '3'
+ base: '3'
+ shift: '#'
+ ralt: '\u0302'
+}
+
+key 4 {
+ label: '4'
+ base: '4'
+ shift: '$'
+ ralt: '\u0306'
+}
+
+key 5 {
+ label: '5'
+ base: '5'
+ shift: '%'
+ ralt: '\u030A'
+}
+
+key 6 {
+ label: '6'
+ base: '6'
+ shift: '^'
+ ralt: '\u0328'
+}
+
+key 7 {
+ label: '7'
+ base: '7'
+ shift: '&'
+ ralt: '\u0300'
+}
+
+key 8 {
+ label: '8'
+ base: '8'
+ shift: '*'
+ ralt: '\u0307'
+}
+
+key 9 {
+ label: '9'
+ base: '9'
+ shift: '('
+ ralt: '\u0301'
+}
+
+key 0 {
+ label: '0'
+ base: '0'
+ shift: ')'
+ ralt: '\u030B'
+}
+
+key MINUS {
+ label: '-'
+ base: '-'
+ shift: '_'
+ ralt: '\u0308'
+ ralt+shift: '\u2013'
+}
+
+key EQUALS {
+ label: '='
+ base: '='
+ shift: '+'
+ ralt: '\u0327'
+ ralt+shift: '\u00b1'
+}
+
+### ROW 2
+
+key Q {
+ label: 'Q'
+ base, capslock+shift: 'q'
+ shift, capslock: 'Q'
+}
+
+key W {
+ label: 'W'
+ base, capslock+shift: 'w'
+ shift, capslock: 'W'
+}
+
+key E {
+ label: 'E'
+ base, capslock+shift: 'e'
+ shift, capslock: 'E'
+ ralt: '\u20ac'
+}
+
+key R {
+ label: 'R'
+ base, capslock+shift: 'r'
+ shift, capslock: 'R'
+}
+
+key T {
+ label: 'T'
+ base, capslock+shift: 't'
+ shift, capslock: 'T'
+}
+
+key Y {
+ label: 'Y'
+ base, capslock+shift: 'y'
+ shift, capslock: 'Y'
+}
+
+key U {
+ label: 'U'
+ base, capslock+shift: 'u'
+ shift, capslock: 'U'
+}
+
+key I {
+ label: 'I'
+ base, capslock+shift: 'i'
+ shift, capslock: 'I'
+}
+
+key O {
+ label: 'O'
+ base, capslock+shift: 'o'
+ shift, capslock: 'O'
+}
+
+key P {
+ label: 'P'
+ base, capslock+shift: 'p'
+ shift, capslock: 'P'
+ ralt: '\u00a7'
+}
+
+key LEFT_BRACKET {
+ label: '\u0102'
+ base, capslock+shift: '\u0103'
+ shift, capslock: '\u0102'
+ ralt: '['
+ ralt+shift: '{'
+}
+
+key RIGHT_BRACKET {
+ label: '\u00ce'
+ base, capslock+shift: '\u00ee'
+ shift, capslock: '\u00ce'
+ ralt: ']'
+ ralt+shift: '}'
+}
+
+### ROW 3
+
+key A {
+ label: 'A'
+ base, capslock+shift: 'a'
+ shift, capslock: 'A'
+}
+
+key S {
+ label: 'S'
+ base, capslock+shift: 's'
+ shift, capslock: 'S'
+ ralt: '\u00df'
+}
+
+key D {
+ label: 'D'
+ base, capslock+shift: 'd'
+ shift, capslock: 'D'
+ ralt: '\u0111'
+ ralt+shift, ralt+capslock: '\u0110'
+ ralt+shift+capslock: '\u0111'
+}
+
+key F {
+ label: 'F'
+ base, capslock+shift: 'f'
+ shift, capslock: 'F'
+}
+
+key G {
+ label: 'G'
+ base, capslock+shift: 'g'
+ shift, capslock: 'G'
+}
+
+key H {
+ label: 'H'
+ base, capslock+shift: 'h'
+ shift, capslock: 'H'
+}
+
+key J {
+ label: 'J'
+ base, capslock+shift: 'j'
+ shift, capslock: 'J'
+}
+
+key K {
+ label: 'K'
+ base, capslock+shift: 'k'
+ shift, capslock: 'K'
+}
+
+key L {
+ label: 'L'
+ base, capslock+shift: 'l'
+ shift, capslock: 'L'
+ ralt: '\u0142'
+ ralt+shift, ralt+capslock: '\u0141'
+ ralt+shift+capslock: '\u0142'
+}
+
+key SEMICOLON {
+ label: '\u0218'
+ base, capslock+shift: '\u0219'
+ shift, capslock: '\u0218'
+ ralt: ';'
+ ralt+shift: ':'
+}
+
+key APOSTROPHE {
+ label: '\u021a'
+ base, capslock+shift: '\u021b'
+ shift, capslock: '\u021a'
+ ralt: '\''
+ ralt+shift: '\u0022'
+}
+
+key BACKSLASH {
+ label: '\u00c2'
+ base, capslock+shift: '\u00e2'
+ shift, capslock: '\u00c2'
+ ralt: '\\'
+ ralt+shift: '|'
+}
+
+### ROW 4
+
+key PLUS {
+ label: '\\'
+ base: '\\'
+ shift: '|'
+}
+
+key Z {
+ label: 'Z'
+ base, capslock+shift: 'z'
+ shift, capslock: 'Z'
+}
+
+key X {
+ label: 'X'
+ base, capslock+shift: 'x'
+ shift, capslock: 'X'
+}
+
+key C {
+ label: 'C'
+ base, capslock+shift: 'c'
+ shift, capslock: 'C'
+ ralt: '\u00a9'
+}
+
+key V {
+ label: 'V'
+ base, capslock+shift: 'v'
+ shift, capslock: 'V'
+}
+
+key B {
+ label: 'B'
+ base, capslock+shift: 'b'
+ shift, capslock: 'B'
+}
+
+key N {
+ label: 'N'
+ base, capslock+shift: 'n'
+ shift, capslock: 'N'
+}
+
+key M {
+ label: 'M'
+ base, capslock+shift: 'm'
+ shift, capslock: 'M'
+}
+
+key COMMA {
+ label: ','
+ base: ','
+ shift: ';'
+ ralt: '<'
+ ralt+shift: '\u00ab'
+}
+
+key PERIOD {
+ label: '.'
+ base: '.'
+ shift: ':'
+ ralt: '>'
+ ralt+shift: '\u00bb'
+}
+
+key SLASH {
+ label: '/'
+ base: '/'
+ shift: '?'
+}
diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml
index 5a911256d9be..bd7cdc481524 100644
--- a/packages/InputDevices/res/values/strings.xml
+++ b/packages/InputDevices/res/values/strings.xml
@@ -164,4 +164,7 @@
<!-- Montenegrin (Cyrillic) keyboard layout label. [CHAR LIMIT=35] -->
<string name="keyboard_layout_montenegrin_cyrillic">Montenegrin (Cyrillic)</string>
+
+ <!-- Romanian keyboard layout label. [CHAR LIMIT=35] -->
+ <string name="keyboard_layout_romanian">Romanian</string>
</resources>
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 93094890418d..9ce9a87a1f9f 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -360,4 +360,11 @@
android:keyboardLayout="@raw/keyboard_layout_serbian_and_montenegrin_cyrillic"
android:keyboardLocale="cnr-Cyrl-ME"
android:keyboardLayoutType="extended" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_romanian"
+ android:label="@string/keyboard_layout_romanian"
+ android:keyboardLayout="@raw/keyboard_layout_romanian"
+ android:keyboardLocale="ro-Latn-RO"
+ android:keyboardLayoutType="qwerty" />
</keyboard-layouts>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index b20117d78230..c99d37bb6ce6 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -19,6 +19,7 @@ package com.android.packageinstaller;
import static android.Manifest.permission;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ARCHIVED_PACKAGES;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import android.app.Activity;
import android.app.DialogFragment;
@@ -53,6 +54,8 @@ public class UnarchiveActivity extends Activity {
@Override
public void onCreate(Bundle icicle) {
+ getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+
super.onCreate(null);
int callingUid = getLaunchedFromUid();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
index 42dd382b98bc..fbb0fa4d6a57 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
@@ -21,10 +21,14 @@ import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.widget.Button;
public class UnarchiveFragment extends DialogFragment implements
DialogInterface.OnClickListener {
+ private Dialog mDialog;
+ private Button mRestoreButton;
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String appTitle = getArguments().getString(UnarchiveActivity.APP_TITLE);
@@ -40,7 +44,32 @@ public class UnarchiveFragment extends DialogFragment implements
dialogBuilder.setPositiveButton(R.string.restore, this);
dialogBuilder.setNegativeButton(android.R.string.cancel, this);
- return dialogBuilder.create();
+ mDialog = dialogBuilder.create();
+ return mDialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mDialog != null) {
+ mRestoreButton = ((AlertDialog) mDialog).getButton(DialogInterface.BUTTON_POSITIVE);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mRestoreButton != null) {
+ mRestoreButton.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mRestoreButton != null) {
+ mRestoreButton.setEnabled(true);
+ }
}
@Override
diff --git a/packages/SettingsLib/DataStore/tests/Android.bp b/packages/SettingsLib/DataStore/tests/Android.bp
index 2e3b42de5b9d..6044eaba5f89 100644
--- a/packages/SettingsLib/DataStore/tests/Android.bp
+++ b/packages/SettingsLib/DataStore/tests/Android.bp
@@ -25,6 +25,5 @@ android_robolectric_test {
java_resource_dirs: ["config"],
instrumentation_for: "SettingsLibDataStoreShell",
coverage_libs: ["SettingsLibDataStore"],
- upstream: true,
strict_mode: false,
}
diff --git a/packages/SettingsLib/Ipc/Android.bp b/packages/SettingsLib/Ipc/Android.bp
index 2c7209a48bbd..bc5a9364279d 100644
--- a/packages/SettingsLib/Ipc/Android.bp
+++ b/packages/SettingsLib/Ipc/Android.bp
@@ -25,7 +25,7 @@ android_library {
name: "SettingsLibIpc-testutils",
srcs: ["testutils/**/*.kt"],
static_libs: [
- "Robolectric_all-target_upstream",
+ "Robolectric_all-target",
"SettingsLibIpc",
"androidx.test.core",
"flag-junit",
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
index f6477e2f052a..dd6743b8595c 100644
--- a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
+++ b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
@@ -68,7 +68,6 @@ android_robolectric_test {
"android.test.mock.stubs.system",
"truth",
],
- upstream: true,
java_resource_dirs: ["config"],
instrumentation_for: "SpaRoboApp",
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
new file mode 100644
index 000000000000..5b7e2a86135a
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.icu.text.DecimalFormat
+import android.icu.text.MeasureFormat
+import android.icu.text.NumberFormat
+import android.icu.text.UnicodeSet
+import android.icu.text.UnicodeSetSpanner
+import android.icu.util.Measure
+import android.text.format.Formatter
+import android.text.format.Formatter.RoundedBytesResult
+import java.math.BigDecimal
+
+class BytesFormatter(resources: Resources) {
+
+ enum class UseCase(val flag: Int) {
+ FileSize(Formatter.FLAG_SI_UNITS),
+ DataUsage(Formatter.FLAG_IEC_UNITS),
+ }
+
+ data class Result(val number: String, val units: String)
+
+ constructor(context: Context) : this(context.resources)
+
+ private val locale = resources.configuration.locales[0]
+
+ fun format(bytes: Long, useCase: UseCase): String {
+ val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
+ val numberFormatter = getNumberFormatter(rounded.fractionDigits)
+ return numberFormatter.formatRoundedBytesResult(rounded)
+ }
+
+ fun formatWithUnits(bytes: Long, useCase: UseCase): Result {
+ val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
+ val numberFormatter = getNumberFormatter(rounded.fractionDigits)
+ val formattedString = numberFormatter.formatRoundedBytesResult(rounded)
+ val formattedNumber = numberFormatter.format(rounded.value)
+ return Result(
+ number = formattedNumber,
+ units = formattedString.removeFirst(formattedNumber),
+ )
+ }
+
+ private fun NumberFormat.formatRoundedBytesResult(rounded: RoundedBytesResult): String {
+ val measureFormatter =
+ MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT, this)
+ return measureFormatter.format(Measure(rounded.value, rounded.units))
+ }
+
+ private fun getNumberFormatter(fractionDigits: Int) =
+ NumberFormat.getInstance(locale).apply {
+ minimumFractionDigits = fractionDigits
+ maximumFractionDigits = fractionDigits
+ isGroupingUsed = false
+ if (this is DecimalFormat) {
+ setRoundingMode(BigDecimal.ROUND_HALF_UP)
+ }
+ }
+
+ private companion object {
+ fun String.removeFirst(removed: String): String =
+ SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString()
+
+ val SPACES_AND_CONTROLS = UnicodeSetSpanner(UnicodeSet("[[:Zs:][:Cf:]]").freeze())
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt
new file mode 100644
index 000000000000..7220848eebff
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BytesFormatterTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val formatter = BytesFormatter(context)
+
+ @Test
+ fun `Zero bytes`() {
+ // Given a byte value of 0, the formatted output should be "0 byte" for both FileSize
+ // and DataUsage UseCases. This verifies special handling of zero values.
+
+ val fileSizeResult = formatter.format(0, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("0 byte")
+
+ val dataUsageResult = formatter.format(0, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("0 byte")
+ }
+
+ @Test
+ fun `Positive bytes`() {
+ // Given a positive byte value (e.g., 1000), the formatted output should be correctly
+ // displayed with appropriate units (e.g., '1.00 kB') for both UseCases.
+
+ val fileSizeResult = formatter.format(1000, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("1.00 kB")
+
+ val dataUsageResult = formatter.format(1024, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("1.00 kB")
+ }
+
+ @Test
+ fun `Large bytes`() {
+ // Given a very large byte value (e.g., Long.MAX_VALUE), the formatted output should be
+ // correctly displayed with the largest unit (e.g., 'PB') for both UseCases.
+
+ val fileSizeResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("9223 PB")
+
+ val dataUsageResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("8192 PB")
+ }
+
+ @Test
+ fun `Bytes requiring rounding`() {
+ // Given byte values that require rounding (e.g., 1512), the formatted output should be
+ // rounded to the appropriate number of decimal places (e.g., '1.51 kB').
+
+ val fileSizeResult = formatter.format(1512, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("1.51 kB")
+
+ val dataUsageResult = formatter.format(1512, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("1.48 kB")
+ }
+
+ @Test
+ fun `FileSize UseCase`() {
+ // When the UseCase is FileSize, the correct units (byte, KB, kB, GB, TB, PB) should
+ // be used.
+ val values =
+ listOf(
+ 1L,
+ 1024L,
+ 1024L * 1024L,
+ 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L * 1024L,
+ )
+ val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB")
+
+ values.zip(expectedUnits).forEach { (value, expectedUnit) ->
+ val result = formatter.format(value, BytesFormatter.UseCase.FileSize)
+ assertThat(result).contains(expectedUnit)
+ }
+ }
+
+ @Test
+ fun `DataUsage UseCase`() {
+ // When the UseCase is DataUsage, the correct units (byte, kB, MB, GB, TB, PB) should
+ // be used.
+ val values =
+ listOf(
+ 1L,
+ 1024L,
+ 1024L * 1024L,
+ 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L * 1024L,
+ )
+ val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB")
+
+ values.zip(expectedUnits).forEach { (value, expectedUnit) ->
+ val result = formatter.format(value, BytesFormatter.UseCase.DataUsage)
+ assertThat(result).contains(expectedUnit)
+ }
+ }
+
+ @Test
+ fun `Fraction digits`() {
+ // The number of fraction digits in the output should be correctly determined based on
+ // the rounded byte value.
+
+ assertThat(formatter.format(1500, BytesFormatter.UseCase.FileSize)).isEqualTo("1.50 kB")
+ assertThat(formatter.format(1050, BytesFormatter.UseCase.FileSize)).isEqualTo("1.05 kB")
+ assertThat(formatter.format(999, BytesFormatter.UseCase.FileSize)).isEqualTo("1.00 kB")
+ }
+
+ @Test
+ fun `Rounding mode`() {
+ // The rounding mode used for formatting should be ROUND_HALF_UP.
+
+ val result = formatter.format(1006, BytesFormatter.UseCase.FileSize)
+
+ assertThat(result).isEqualTo("1.01 kB") // Ensure rounding mode is effective
+ }
+
+ @Test
+ fun `Grouping separator`() {
+ // Grouping separators should not be used in the formatted output.
+
+ val result = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize)
+
+ assertThat(result).isEqualTo("9223 PB")
+ }
+
+ @Test
+ fun `Format with units`() {
+ // Verify that the `formatWithUnits` method correctly formats the given bytes with the
+ // specified units.
+
+ val resultByte = formatter.formatWithUnits(0, BytesFormatter.UseCase.FileSize)
+ assertThat(resultByte).isEqualTo(BytesFormatter.Result("0", "byte"))
+
+ val resultKb = formatter.formatWithUnits(1000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultKb).isEqualTo(BytesFormatter.Result("1.00", "kB"))
+
+ val resultMb = formatter.formatWithUnits(479_999_999, BytesFormatter.UseCase.FileSize)
+ assertThat(resultMb).isEqualTo(BytesFormatter.Result("480", "MB"))
+
+ val resultGb = formatter.formatWithUnits(20_100_000_000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultGb).isEqualTo(BytesFormatter.Result("20.10", "GB"))
+
+ val resultTb =
+ formatter.formatWithUnits(300_100_000_000_000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultTb).isEqualTo(BytesFormatter.Result("300", "TB"))
+
+ val resultPb =
+ formatter.formatWithUnits(1000_000_000_000_000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultPb).isEqualTo(BytesFormatter.Result("1.00", "PB"))
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 145b62cd12b5..68e9fe703090 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -73,6 +73,10 @@ public class BluetoothUtils {
private static final Set<Integer> SA_PROFILES =
ImmutableSet.of(
BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID);
+ private static final List<Integer> BLUETOOTH_DEVICE_CLASS_HEADSET =
+ List.of(
+ BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES,
+ BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET);
private static final String TEMP_BOND_TYPE = "TEMP_BOND_TYPE";
private static final String TEMP_BOND_DEVICE_METADATA_VALUE = "le_audio_sharing";
@@ -390,6 +394,19 @@ public class BluetoothUtils {
return false;
}
+ /** Checks whether the bluetooth device is a headset. */
+ public static boolean isHeadset(@NonNull BluetoothDevice bluetoothDevice) {
+ String deviceType =
+ BluetoothUtils.getStringMetaData(
+ bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
+ if (!TextUtils.isEmpty(deviceType)) {
+ return BluetoothDevice.DEVICE_TYPE_HEADSET.equals(deviceType)
+ || BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.equals(deviceType);
+ }
+ BluetoothClass btClass = bluetoothDevice.getBluetoothClass();
+ return btClass != null && BLUETOOTH_DEVICE_CLASS_HEADSET.contains(btClass.getDeviceClass());
+ }
+
/** Create an Icon pointing to a drawable. */
public static IconCompat createIconWithDrawable(Drawable drawable) {
Bitmap bitmap;
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index 81358ca168d0..117ca85c2761 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -65,7 +65,6 @@ android_robolectric_test {
test_options: {
timeout: 36000,
},
- upstream: true,
strict_mode: false,
}
@@ -100,10 +99,10 @@ java_library {
plugins: [
"auto_value_plugin_1.9",
"auto_value_builder_plugin_1.9",
- "Robolectric_processor_upstream",
+ "Robolectric_processor",
],
libs: [
- "Robolectric_all-target_upstream",
+ "Robolectric_all-target",
"mockito-robolectric-prebuilt",
"truth",
],
diff --git a/packages/SettingsLib/tests/robotests/fragment/Android.bp b/packages/SettingsLib/tests/robotests/fragment/Android.bp
index 3e67156af0c4..0214874979f3 100644
--- a/packages/SettingsLib/tests/robotests/fragment/Android.bp
+++ b/packages/SettingsLib/tests/robotests/fragment/Android.bp
@@ -28,13 +28,13 @@ java_library {
//"-J-verbose",
],
libs: [
- "Robolectric_all-target_upstream",
+ "Robolectric_all-target",
"androidx.fragment_fragment",
],
plugins: [
"auto_value_plugin_1.9",
"auto_value_builder_plugin_1.9",
- "Robolectric_processor_upstream",
+ "Robolectric_processor",
],
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index d49447f05011..cafe19ff9a9b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -80,7 +80,9 @@ public class BluetoothUtilsTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private CachedBluetoothDevice mCachedBluetoothDevice;
- @Mock private BluetoothDevice mBluetoothDevice;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private BluetoothDevice mBluetoothDevice;
+
@Mock private AudioManager mAudioManager;
@Mock private PackageManager mPackageManager;
@Mock private LeAudioProfile mA2dpProfile;
@@ -399,6 +401,38 @@ public class BluetoothUtilsTest {
}
@Test
+ public void isHeadset_metadataMatched_returnTrue() {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE))
+ .thenReturn(BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.getBytes());
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isHeadset_metadataNotMatched_returnFalse() {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE))
+ .thenReturn(BluetoothDevice.DEVICE_TYPE_CARKIT.getBytes());
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isHeadset_btClassMatched_returnTrue() {
+ when(mBluetoothDevice.getBluetoothClass().getDeviceClass())
+ .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES);
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isHeadset_btClassNotMatched_returnFalse() {
+ when(mBluetoothDevice.getBluetoothClass().getDeviceClass())
+ .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER);
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse();
+ }
+
+ @Test
public void isAvailableMediaBluetoothDevice_isConnectedLeAudioDevice_returnTrue() {
when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index b9f8c714175e..1fc1f05ae149 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -186,11 +186,6 @@ public class SecureSettings {
Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT,
Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
Settings.Secure.NAVIGATION_MODE,
- Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED,
Settings.Secure.SKIP_GESTURE_COUNT,
Settings.Secure.SKIP_TOUCH_COUNT,
Settings.Secure.SILENCE_ALARMS_GESTURE_COUNT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 7c5e577b1d93..d0e88d5d6a3c 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -284,11 +284,6 @@ public class SecureSettingsValidators {
new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_BACK_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_HOME_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.AWARE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SKIP_GESTURE_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.SKIP_TOUCH_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index b88ae3751d22..227fff59d327 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -85,6 +85,9 @@ filegroup {
filegroup {
name: "SystemUI-tests-broken-robofiles-run",
srcs: [
+ "tests/src/**/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt",
+ "tests/src/**/systemui/power/PowerNotificationWarningsTest.java",
+ "tests/src/**/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt",
"tests/src/**/systemui/dreams/touch/CommunalTouchHandlerTest.java",
"tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt",
"tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt",
@@ -841,7 +844,6 @@ android_robolectric_test {
"androidx.test.ext.truth",
],
- upstream: true,
instrumentation_for: "SystemUIRobo-stub",
java_resource_dirs: ["tests/robolectric/config"],
@@ -879,7 +881,6 @@ android_robolectric_test {
"androidx.test.ext.truth",
],
- upstream: true,
instrumentation_for: "SystemUIRobo-stub",
java_resource_dirs: ["tests/robolectric/config"],
@@ -916,6 +917,7 @@ android_ravenwood_test {
"android.test.mock.impl",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
plugins: [
"dagger2-compiler",
],
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 99d704f1eca8..51ea5298fbb8 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -413,7 +413,8 @@
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
tools:replace="android:appComponentFactory"
- android:appComponentFactory=".PhoneSystemUIAppComponentFactory">
+ android:appComponentFactory=".PhoneSystemUIAppComponentFactory"
+ android:enableOnBackInvokedCallback="true">
<!-- Keep theme in sync with SystemUIApplication.onCreate().
Setting the theme on the application does not affect views inflated by services.
The application theme is set again from onCreate to take effect for those views. -->
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index 46eb9e1c2330..ee918c275b7b 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -2,29 +2,8 @@ package: "com.android.systemui"
container: "system"
flag {
- name: "predictive_back_sysui"
- namespace: "systemui"
- description: "Predictive Back Dispatching for SysUI"
- bug: "327737297"
-}
-
-flag {
name: "predictive_back_animate_shade"
namespace: "systemui"
description: "Enable Shade Animations"
bug: "327732946"
}
-
-flag {
- name: "predictive_back_animate_bouncer"
- namespace: "systemui"
- description: "Enable Predictive Back Animation in Bouncer"
- bug: "327733487"
-}
-
-flag {
- name: "predictive_back_animate_dialogs"
- namespace: "systemui"
- description: "Enable Predictive Back Animation for SysUI dialogs"
- bug: "327721544"
-}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
index 91fad4fb6dc2..56d85ab8aca0 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
@@ -107,7 +107,7 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub {
IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishCallback) {
logD("mergeAnimation - " + info);
- mHandler.post(this::cancel);
+ cancel();
}
@Override
@@ -129,7 +129,7 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub {
@Override
public void onTransitionConsumed(IBinder transition, boolean aborted) {
logD("onTransitionConsumed - aborted: " + aborted);
- mHandler.post(this::cancel);
+ cancel();
}
private void startAnimationInternal(
@@ -342,11 +342,14 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub {
mFinishCallback = null;
}
- private void cancel() {
+ public void cancel() {
logD("cancel()");
- if (mAnimator != null) {
- mAnimator.cancel();
- }
+ mHandler.post(
+ () -> {
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ });
}
private static void logD(String msg) {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
index 6d6aa8895ed0..cb3dfb9e78e7 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -43,6 +43,7 @@ import java.util.function.Supplier;
/**
* A session object that holds origin transition states for starting an activity from an on-screen
* UI component and smoothly transitioning back from the activity to the same UI component.
+ *
* @hide
*/
public class OriginTransitionSession {
@@ -143,6 +144,12 @@ public class OriginTransitionSession {
logE("Unable to cancel origin transition!", e);
}
}
+ if (mEntryTransition instanceof OriginRemoteTransition) {
+ ((OriginRemoteTransition) mEntryTransition).cancel();
+ }
+ if (mExitTransition instanceof OriginRemoteTransition) {
+ ((OriginRemoteTransition) mExitTransition).cancel();
+ }
}
private boolean hasEntryTransition() {
@@ -182,6 +189,7 @@ public class OriginTransitionSession {
/**
* A builder to build a {@link OriginTransitionSession}.
+ *
* @hide
*/
public static class Builder {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
index 7c219c6ca921..2e8f92839fa6 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
@@ -24,13 +24,15 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver.OnDrawListener;
+import android.view.ViewTreeObserver;
import java.util.ArrayList;
import java.util.List;
@@ -51,8 +53,9 @@ public class ViewUIComponent implements UIComponent {
private final Path mClippingPath = new Path();
private final Outline mClippingOutline = new Outline();
- private final OnDrawListener mOnDrawListener = this::postDraw;
+ private final LifecycleListener mLifecycleListener = new LifecycleListener();
private final View mView;
+ private final Handler mMainHandler;
@Nullable private SurfaceControl mSurfaceControl;
@Nullable private Surface mSurface;
@@ -62,6 +65,7 @@ public class ViewUIComponent implements UIComponent {
public ViewUIComponent(View view) {
mView = view;
+ mMainHandler = new Handler(Looper.getMainLooper());
}
/**
@@ -110,11 +114,11 @@ public class ViewUIComponent implements UIComponent {
t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
// Make sure view draw triggers surface draw.
- mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+ mLifecycleListener.register();
// Make the view invisible AFTER the surface is shown.
t.addTransactionCommittedListener(
- mView::post,
+ this::post,
() -> {
logD("Surface attached!");
forceDraw();
@@ -129,14 +133,14 @@ public class ViewUIComponent implements UIComponent {
SurfaceControl sc = mSurfaceControl;
mSurface = null;
mSurfaceControl = null;
- mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+ mLifecycleListener.unregister();
// Restore view visibility
mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
// Clean up surfaces.
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(sc, null)
.addTransactionCommittedListener(
- mView::post,
+ this::post,
() -> {
s.release();
sc.release();
@@ -269,7 +273,66 @@ public class ViewUIComponent implements UIComponent {
return;
}
mDirty = true;
- mView.post(this::draw);
+ post(this::draw);
+ }
+
+ private void post(Runnable r) {
+ if (mView.isAttachedToWindow()) {
+ mView.post(r);
+ } else {
+ // If the view is detached from window, {@code View.post()} will postpone the action
+ // until the view is attached again. However, we don't know if the view will be attached
+ // again, so we post the action to the main thread in this case. This could lead to race
+ // condition if the attachment change caused a thread switching, and it's the caller's
+ // responsibility to ensure the window attachment state doesn't change unexpectedly.
+ if (DEBUG) {
+ Log.w(TAG, mView + " is not attached. Posting action to main thread!");
+ }
+ mMainHandler.post(r);
+ }
+ }
+
+ /** A listener for monitoring view life cycles. */
+ private class LifecycleListener
+ implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener {
+ private boolean mRegistered;
+
+ @Override
+ public void onDraw() {
+ // View draw should trigger surface draw.
+ postDraw();
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ // empty
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ Log.w(
+ TAG,
+ v + " is detached from the window. Unregistering the life cycle listener ...");
+ unregister();
+ }
+
+ public void register() {
+ if (mRegistered) {
+ return;
+ }
+ mRegistered = true;
+ mView.getViewTreeObserver().addOnDrawListener(this);
+ mView.addOnAttachStateChangeListener(this);
+ }
+
+ public void unregister() {
+ if (!mRegistered) {
+ return;
+ }
+ mRegistered = false;
+ mView.getViewTreeObserver().removeOnDrawListener(this);
+ mView.removeOnAttachStateChangeListener(this);
+ }
}
/** @hide */
@@ -278,34 +341,33 @@ public class ViewUIComponent implements UIComponent {
@Override
public Transaction setAlpha(ViewUIComponent ui, float alpha) {
- mChanges.add(() -> ui.mView.post(() -> ui.setAlpha(alpha)));
+ mChanges.add(() -> ui.post(() -> ui.setAlpha(alpha)));
return this;
}
@Override
public Transaction setVisible(ViewUIComponent ui, boolean visible) {
- mChanges.add(() -> ui.mView.post(() -> ui.setVisible(visible)));
+ mChanges.add(() -> ui.post(() -> ui.setVisible(visible)));
return this;
}
@Override
public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
- mChanges.add(() -> ui.mView.post(() -> ui.setBounds(bounds)));
+ mChanges.add(() -> ui.post(() -> ui.setBounds(bounds)));
return this;
}
@Override
public Transaction attachToTransitionLeash(
ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
- mChanges.add(
- () -> ui.mView.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
+ mChanges.add(() -> ui.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
return this;
}
@Override
public Transaction detachFromTransitionLeash(
ViewUIComponent ui, Executor executor, Runnable onDone) {
- mChanges.add(() -> ui.mView.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
+ mChanges.add(() -> ui.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
return this;
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
deleted file mode 100644
index 1c9dabbb0e07..000000000000
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.android.systemui.animation
-
-interface AnimationFeatureFlags {
- val isPredictiveBackQsDialogAnim: Boolean
- get() = false
-}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index 907c39d842ce..c88c4ebb1a8d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -59,13 +59,8 @@ constructor(
private val mainExecutor: Executor,
private val callback: Callback,
private val interactionJankMonitor: InteractionJankMonitor,
- private val featureFlags: AnimationFeatureFlags,
private val transitionAnimator: TransitionAnimator =
- TransitionAnimator(
- mainExecutor,
- TIMINGS,
- INTERPOLATORS,
- ),
+ TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS),
private val isForTesting: Boolean = false,
) {
private companion object {
@@ -219,7 +214,7 @@ constructor(
dialog: Dialog,
view: View,
cuj: DialogCuj? = null,
- animateBackgroundBoundsChange: Boolean = false
+ animateBackgroundBoundsChange: Boolean = false,
) {
val controller = Controller.fromView(view, cuj)
if (controller == null) {
@@ -245,7 +240,7 @@ constructor(
fun show(
dialog: Dialog,
controller: Controller,
- animateBackgroundBoundsChange: Boolean = false
+ animateBackgroundBoundsChange: Boolean = false,
) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException(
@@ -263,15 +258,14 @@ constructor(
val controller =
animatedParent?.dialogContentWithBackground?.let {
Controller.fromView(it, controller.cuj)
- }
- ?: controller
+ } ?: controller
// Make sure we don't run the launch animation from the same source twice at the same time.
if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
Log.e(
TAG,
"Not running dialog launch animation from source as it is already expanded into a" +
- " dialog"
+ " dialog",
)
dialog.show()
return
@@ -288,7 +282,6 @@ constructor(
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
parentAnimatedDialog = animatedParent,
forceDisableSynchronization = isForTesting,
- featureFlags = featureFlags,
)
openedDialogs.add(animatedDialog)
@@ -305,7 +298,7 @@ constructor(
dialog: Dialog,
animateFrom: Dialog,
cuj: DialogCuj? = null,
- animateBackgroundBoundsChange: Boolean = false
+ animateBackgroundBoundsChange: Boolean = false,
) {
val view =
openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
@@ -313,7 +306,7 @@ constructor(
Log.w(
TAG,
"Showing dialog $dialog normally as the dialog it is shown from was not shown " +
- "using DialogTransitionAnimator"
+ "using DialogTransitionAnimator",
)
dialog.show()
return
@@ -323,7 +316,7 @@ constructor(
dialog,
view,
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
- cuj = cuj
+ cuj = cuj,
)
}
@@ -346,8 +339,7 @@ constructor(
val animatedDialog =
openedDialogs.firstOrNull {
it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl
- }
- ?: return null
+ } ?: return null
return createActivityTransitionController(animatedDialog, cujType)
}
@@ -373,7 +365,7 @@ constructor(
private fun createActivityTransitionController(
animatedDialog: AnimatedDialog,
- cujType: Int? = null
+ cujType: Int? = null,
): ActivityTransitionAnimator.Controller? {
// At this point, we know that the intent of the caller is to dismiss the dialog to show
// an app, so we disable the exit animation into the source because we will never want to
@@ -440,7 +432,7 @@ constructor(
}
private fun disableDialogDismiss() {
- dialog.setDismissOverride { /* Do nothing */}
+ dialog.setDismissOverride { /* Do nothing */ }
}
private fun enableDialogDismiss() {
@@ -530,7 +522,6 @@ private class AnimatedDialog(
* Whether synchronization should be disabled, which can be useful if we are running in a test.
*/
private val forceDisableSynchronization: Boolean,
- private val featureFlags: AnimationFeatureFlags,
) {
/**
* The DecorView of this dialog window.
@@ -643,8 +634,7 @@ private class AnimatedDialog(
originalDialogBackgroundColor =
GhostedViewTransitionAnimatorController.findGradientDrawable(background)
?.color
- ?.defaultColor
- ?: Color.BLACK
+ ?.defaultColor ?: Color.BLACK
// Make the background view invisible until we start the animation. We use the transition
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
@@ -700,7 +690,7 @@ private class AnimatedDialog(
oldLeft: Int,
oldTop: Int,
oldRight: Int,
- oldBottom: Int
+ oldBottom: Int,
) {
dialogContentWithBackground.removeOnLayoutChangeListener(this)
@@ -717,9 +707,7 @@ private class AnimatedDialog(
// the dialog.
dialog.setDismissOverride(this::onDialogDismissed)
- if (featureFlags.isPredictiveBackQsDialogAnim) {
- dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
- }
+ dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
// Show the dialog.
dialog.show()
@@ -815,7 +803,7 @@ private class AnimatedDialog(
if (hasInstrumentedJank) {
interactionJankMonitor.end(controller.cuj!!.cujType)
}
- }
+ },
)
}
@@ -888,14 +876,14 @@ private class AnimatedDialog(
onAnimationFinished(true /* instantDismiss */)
onDialogDismissed(this@AnimatedDialog)
}
- }
+ },
)
}
private fun startAnimation(
isLaunching: Boolean,
onLaunchAnimationStart: () -> Unit = {},
- onLaunchAnimationEnd: () -> Unit = {}
+ onLaunchAnimationEnd: () -> Unit = {},
) {
// Create 2 controllers to animate both the dialog and the source.
val startController =
@@ -969,7 +957,7 @@ private class AnimatedDialog(
override fun onTransitionAnimationProgress(
state: TransitionAnimator.State,
progress: Float,
- linearProgress: Float
+ linearProgress: Float,
) {
startController.onTransitionAnimationProgress(state, progress, linearProgress)
@@ -1026,7 +1014,7 @@ private class AnimatedDialog(
oldLeft: Int,
oldTop: Int,
oldRight: Int,
- oldBottom: Int
+ oldBottom: Int,
) {
// Don't animate if bounds didn't actually change.
if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 46e0efa79ef5..183929ca6c6a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -43,6 +43,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.overscroll
@@ -180,10 +181,12 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace(
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
) {
+
val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
var scrollOffset by remember { mutableFloatStateOf(0f) }
- val minScrollOffset = -(stackScrollView.getHeadsUpInset().toFloat())
+ val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
+ val minScrollOffset = -headsUpInset
val maxScrollOffset = 0f
val scrollableState = rememberScrollableState { delta ->
@@ -241,6 +244,12 @@ fun ContentScope.SnoozeableHeadsUpNotificationSpace(
)
}
+/** Y position of the HUNs at rest, when the shade is closed. */
+@Composable
+fun headsUpTopInset(): Dp =
+ WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() +
+ dimensionResource(R.dimen.heads_up_status_bar_padding)
+
/** Adds the space where notification stack should appear in the scene. */
@Composable
fun ContentScope.ConstrainedNotificationStack(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 75226b3e9a57..2e1100a219cf 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -19,6 +19,7 @@ package com.android.systemui.people.ui.compose
import android.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -31,7 +32,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -45,6 +45,7 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -60,7 +61,11 @@ import com.android.systemui.res.R
* the Activity/Fragment/View hosting this Composable once a result is available.
*/
@Composable
-fun PeopleScreen(viewModel: PeopleViewModel, onResult: (PeopleViewModel.Result) -> Unit) {
+fun PeopleScreen(
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ modifier: Modifier = Modifier,
+) {
val priorityTiles by viewModel.priorityTiles.collectAsStateWithLifecycle()
val recentTiles by viewModel.recentTiles.collectAsStateWithLifecycle()
@@ -74,7 +79,7 @@ fun PeopleScreen(viewModel: PeopleViewModel, onResult: (PeopleViewModel.Result)
}
}
- Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
+ Surface(color = MaterialTheme.colorScheme.background, modifier = modifier.fillMaxSize()) {
if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel.onTileClicked)
} else {
@@ -88,9 +93,10 @@ private fun PeopleScreenWithConversations(
priorityTiles: List<PeopleTileViewModel>,
recentTiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
+ modifier: Modifier = Modifier,
) {
Column(
- Modifier.fillMaxSize().safeDrawingPadding().sysuiResTag("top_level_with_conversations")
+ modifier.fillMaxSize().safeDrawingPadding().sysuiResTag("top_level_with_conversations")
) {
Column(
Modifier.fillMaxWidth().padding(PeopleSpacePadding),
@@ -139,28 +145,32 @@ private fun ConversationList(
@StringRes headerTextResource: Int,
tiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
+ modifier: Modifier = Modifier,
) {
- Text(
- stringResource(headerTextResource),
- Modifier.padding(start = 16.dp),
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.primary,
- )
-
- Spacer(Modifier.height(10.dp))
-
- tiles.forEachIndexed { index, tile ->
- if (index > 0) {
- HorizontalDivider(color = MaterialTheme.colorScheme.background, thickness = 2.dp)
- }
-
- key(tile.key.toString()) {
- Tile(
- tile,
- onTileClicked,
- withTopCornerRadius = index == 0,
- withBottomCornerRadius = index == tiles.lastIndex,
- )
+ val largeCornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
+ val smallCornerRadius = 4.dp
+
+ fun topRadius(i: Int): Dp = if (i == 0) largeCornerRadius else smallCornerRadius
+ fun bottomRadius(i: Int): Dp =
+ if (i == tiles.lastIndex) largeCornerRadius else smallCornerRadius
+
+ Column(modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ Text(
+ stringResource(headerTextResource),
+ Modifier.padding(start = 16.dp, bottom = 8.dp),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ )
+
+ tiles.forEachIndexed { index, tile ->
+ key(tile.key.toString()) {
+ Tile(
+ tile,
+ onTileClicked,
+ topCornerRadius = topRadius(index),
+ bottomCornerRadius = bottomRadius(index),
+ )
+ }
}
}
}
@@ -169,14 +179,12 @@ private fun ConversationList(
private fun Tile(
tile: PeopleTileViewModel,
onTileClicked: (PeopleTileViewModel) -> Unit,
- withTopCornerRadius: Boolean,
- withBottomCornerRadius: Boolean,
+ topCornerRadius: Dp,
+ bottomCornerRadius: Dp,
+ modifier: Modifier = Modifier,
) {
- val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
- val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
- val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
-
Surface(
+ modifier,
color = MaterialTheme.colorScheme.secondaryContainer,
shape =
RoundedCornerShape(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
index 527314d8c03e..d4dea65a7c73 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -44,9 +44,9 @@ import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
@Composable
-internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit) {
+internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit, modifier: Modifier = Modifier) {
Column(
- Modifier.fillMaxSize().safeDrawingPadding().padding(PeopleSpacePadding),
+ modifier.fillMaxSize().safeDrawingPadding().padding(PeopleSpacePadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
@@ -74,8 +74,9 @@ internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit) {
}
@Composable
-private fun ExampleTile() {
+private fun ExampleTile(modifier: Modifier = Modifier) {
Surface(
+ modifier,
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index e4f4df386583..9ee25c3404ec 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -24,6 +24,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
@@ -34,6 +35,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.headsUpTopInset
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default
@@ -78,6 +80,8 @@ constructor(
}
}
+ val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
+
LaunchedEffect(isIdleAndNotOccluded) {
// Wait for being Idle on this Scene, otherwise LaunchedEffect would fire too soon,
// and another transition could override the NSSL stack bounds.
@@ -86,7 +90,7 @@ constructor(
// and not to confuse the StackScrollAlgorithm when it displays a HUN over GONE.
notificationStackScrolLView.get().apply {
// use -headsUpInset to allow HUN translation outside bounds for snoozing
- setStackTop(-getHeadsUpInset().toFloat())
+ setStackTop(-headsUpInset)
setStackCutoff(0f)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 568a358e4a7e..8153586efbca 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -232,6 +232,8 @@ fun MutableSceneTransitionLayoutState(
canShowOverlay: (OverlayKey) -> Boolean = { true },
canHideOverlay: (OverlayKey) -> Boolean = { true },
canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
+ onTransitionStart: (TransitionState.Transition) -> Unit = {},
+ onTransitionEnd: (TransitionState.Transition) -> Unit = {},
): MutableSceneTransitionLayoutState {
return MutableSceneTransitionLayoutStateImpl(
initialScene,
@@ -241,6 +243,8 @@ fun MutableSceneTransitionLayoutState(
canShowOverlay,
canHideOverlay,
canReplaceOverlay,
+ onTransitionStart,
+ onTransitionEnd,
)
}
@@ -252,7 +256,11 @@ internal class MutableSceneTransitionLayoutStateImpl(
internal val canChangeScene: (SceneKey) -> Boolean = { true },
internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
- internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
+ internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
+ true
+ },
+ private val onTransitionStart: (TransitionState.Transition) -> Unit = {},
+ private val onTransitionEnd: (TransitionState.Transition) -> Unit = {},
) : MutableSceneTransitionLayoutState {
private val creationThread: Thread = Thread.currentThread()
@@ -367,9 +375,11 @@ internal class MutableSceneTransitionLayoutStateImpl(
startTransitionInternal(transition, chain)
// Run the transition until it is finished.
+ onTransitionStart(transition)
transition.runInternal()
} finally {
finishTransition(transition)
+ onTransitionEnd(transition)
}
}
@@ -384,14 +394,10 @@ internal class MutableSceneTransitionLayoutStateImpl(
val toContent = transition.toContent
// Update the transition specs.
- transition.transformationSpec =
- transitions
- .transitionSpec(fromContent, toContent, key = transition.key)
- .transformationSpec(transition)
- transition.previewTransformationSpec =
- transitions
- .transitionSpec(fromContent, toContent, key = transition.key)
- .previewTransformationSpec(transition)
+ val spec = transitions.transitionSpec(fromContent, toContent, key = transition.key)
+ transition._cuj = spec.cuj
+ transition.transformationSpec = spec.transformationSpec(transition)
+ transition.previewTransformationSpec = spec.previewTransformationSpec(transition)
}
private fun startTransitionInternal(transition: TransitionState.Transition, chain: Boolean) {
@@ -411,9 +417,7 @@ internal class MutableSceneTransitionLayoutStateImpl(
if (tooManyTransitions) logTooManyTransitions()
// Force finish all transitions.
- while (currentTransitions.isNotEmpty()) {
- finishTransition(transitionStates[0] as TransitionState.Transition)
- }
+ currentTransitions.fastForEach { finishTransition(it) }
// We finished all transitions, so we are now idle. We remove this state so that
// we end up only with the new transition after appending it.
@@ -475,46 +479,36 @@ internal class MutableSceneTransitionLayoutStateImpl(
// Mark this transition as finished.
finishedTransitions.add(transition)
- // Keep a reference to the last transition, in case we remove all transitions and should
- // settle to Idle.
+ if (finishedTransitions.size != transitionStates.size) {
+ // Some transitions were not finished, so we won't settle to idle.
+ return
+ }
+
+ // Keep a reference to the last transition, in case all transitions are finished and we
+ // should settle to Idle.
val lastTransition = transitionStates.last()
- // Remove all first n finished transitions.
- var i = 0
- val nStates = transitionStates.size
- while (i < nStates) {
- val t = transitionStates[i]
- if (!finishedTransitions.contains(t)) {
- // Stop here.
- break
+ transitionStates.fastForEach { state ->
+ if (!finishedTransitions.contains(state)) {
+ // Some transitions were not finished, so we won't settle to idle.
+ return
}
-
- // Remove the transition from the set of finished transitions.
- finishedTransitions.remove(t)
- i++
}
- // If all transitions are finished, we are idle.
- if (i == nStates) {
- check(finishedTransitions.isEmpty())
- val idle =
- TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
- Log.i(TAG, "all transitions finished. idle=$idle")
- this.transitionStates = listOf(idle)
- } else if (i > 0) {
- this.transitionStates = transitionStates.subList(fromIndex = i, toIndex = nStates)
- }
+ val idle = TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
+ Log.i(TAG, "all transitions finished. idle=$idle")
+ finishedTransitions.clear()
+ this.transitionStates = listOf(idle)
}
override fun snapToScene(scene: SceneKey, currentOverlays: Set<OverlayKey>) {
checkThread()
// Force finish all transitions.
- while (currentTransitions.isNotEmpty()) {
- finishTransition(transitionStates[0] as TransitionState.Transition)
- }
+ currentTransitions.fastForEach { finishTransition(it) }
check(transitionStates.size == 1)
+ check(currentTransitions.isEmpty())
transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 756d71c1b5cf..ff8efc28aa21 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -29,6 +29,7 @@ import com.android.compose.animation.scene.transformation.PropertyTransformation
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.TransformationMatcher
import com.android.compose.animation.scene.transformation.TransformationWithRange
+import com.android.internal.jank.Cuj.CujType
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions
@@ -111,7 +112,15 @@ internal constructor(
}
private fun defaultTransition(from: ContentKey, to: ContentKey) =
- TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider)
+ TransitionSpecImpl(
+ key = null,
+ from,
+ to,
+ cuj = null,
+ previewTransformationSpec = null,
+ reversePreviewTransformationSpec = null,
+ TransformationSpec.EmptyProvider,
+ )
companion object {
internal val DefaultSwipeSpec =
@@ -147,6 +156,9 @@ internal interface TransitionSpec {
*/
val to: ContentKey?
+ /** The CUJ covered by this transition. */
+ @CujType val cuj: Int?
+
/**
* Return a reversed version of this [TransitionSpec] for a transition going from [to] to
* [from].
@@ -213,6 +225,7 @@ internal class TransitionSpecImpl(
override val key: TransitionKey?,
override val from: ContentKey?,
override val to: ContentKey?,
+ override val cuj: Int?,
private val previewTransformationSpec:
((TransitionState.Transition) -> TransformationSpecImpl)? =
null,
@@ -226,6 +239,7 @@ internal class TransitionSpecImpl(
key = key,
from = to,
to = from,
+ cuj = cuj,
previewTransformationSpec = reversePreviewTransformationSpec,
reversePreviewTransformationSpec = previewTransformationSpec,
transformationSpec = { transition ->
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index fda6fab6229a..998054ef6c9e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.Transformation
+import com.android.internal.jank.Cuj.CujType
/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
@@ -64,6 +65,7 @@ interface SceneTransitionsBuilder {
fun to(
to: ContentKey,
key: TransitionKey? = null,
+ @CujType cuj: Int? = null,
preview: (TransitionBuilder.() -> Unit)? = null,
reversePreview: (TransitionBuilder.() -> Unit)? = null,
builder: TransitionBuilder.() -> Unit = {},
@@ -90,6 +92,7 @@ interface SceneTransitionsBuilder {
from: ContentKey,
to: ContentKey? = null,
key: TransitionKey? = null,
+ @CujType cuj: Int? = null,
preview: (TransitionBuilder.() -> Unit)? = null,
reversePreview: (TransitionBuilder.() -> Unit)? = null,
builder: TransitionBuilder.() -> Unit = {},
@@ -146,6 +149,9 @@ interface TransitionBuilder : BaseTransitionBuilder {
*/
var swipeSpec: SpringSpec<Float>?
+ /** The CUJ associated to this transitions. */
+ @CujType var cuj: Int?
+
/**
* Define a timestamp-based range for the transformations inside [builder].
*
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index a1649964ec13..7ca521513714 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -37,6 +37,7 @@ import com.android.compose.animation.scene.transformation.Transformation
import com.android.compose.animation.scene.transformation.TransformationMatcher
import com.android.compose.animation.scene.transformation.TransformationRange
import com.android.compose.animation.scene.transformation.Translate
+import com.android.internal.jank.Cuj.CujType
internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
val impl = SceneTransitionsBuilderImpl().apply(builder)
@@ -52,28 +53,47 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
override fun to(
to: ContentKey,
key: TransitionKey?,
+ @CujType cuj: Int?,
preview: (TransitionBuilder.() -> Unit)?,
reversePreview: (TransitionBuilder.() -> Unit)?,
builder: TransitionBuilder.() -> Unit,
) {
- transition(from = null, to = to, key = key, preview, reversePreview, builder)
+ transition(
+ from = null,
+ to = to,
+ key = key,
+ cuj = cuj,
+ preview = preview,
+ reversePreview = reversePreview,
+ builder = builder,
+ )
}
override fun from(
from: ContentKey,
to: ContentKey?,
key: TransitionKey?,
+ @CujType cuj: Int?,
preview: (TransitionBuilder.() -> Unit)?,
reversePreview: (TransitionBuilder.() -> Unit)?,
builder: TransitionBuilder.() -> Unit,
) {
- transition(from = from, to = to, key = key, preview, reversePreview, builder)
+ transition(
+ from = from,
+ to = to,
+ key = key,
+ cuj = cuj,
+ preview = preview,
+ reversePreview = reversePreview,
+ builder = builder,
+ )
}
private fun transition(
from: ContentKey?,
to: ContentKey?,
key: TransitionKey?,
+ @CujType cuj: Int?,
preview: (TransitionBuilder.() -> Unit)?,
reversePreview: (TransitionBuilder.() -> Unit)?,
builder: TransitionBuilder.() -> Unit,
@@ -93,9 +113,10 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
val spec =
TransitionSpecImpl(
- key,
- from,
- to,
+ key = key,
+ from = from,
+ to = to,
+ cuj = cuj,
previewTransformationSpec = preview?.let { { t -> transformationSpec(t, it) } },
reversePreviewTransformationSpec =
reversePreview?.let { { t -> transformationSpec(t, it) } },
@@ -190,6 +211,7 @@ internal class TransitionBuilderImpl(override val transition: TransitionState.Tr
override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
override var swipeSpec: SpringSpec<Float>? = null
override var distance: UserActionDistance? = null
+ override var cuj: Int? = null
private val durationMillis: Int by lazy {
val spec = spec
if (spec !is DurationBasedAnimationSpec) {
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 e7ca51114b93..712af56ee1bc 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
@@ -32,6 +32,7 @@ import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransitionKey
+import com.android.internal.jank.Cuj.CujType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -237,6 +238,11 @@ sealed interface TransitionState {
/** Whether user input is currently driving the transition. */
abstract val isUserInputOngoing: Boolean
+ /** The CUJ covered by this transition. */
+ @CujType
+ val cuj: Int?
+ get() = _cuj
+
/**
* The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
* also be less than `0` or greater than `1` when using transitions with a spring
@@ -251,13 +257,15 @@ sealed interface TransitionState {
internal open val isInPreviewStage: Boolean = false
/**
- * The current [TransformationSpecImpl] associated to this transition.
+ * The current [TransformationSpecImpl] and other values associated to this transition from
+ * the spec.
*
* Important: These will be set exactly once, when this transition is
* [started][MutableSceneTransitionLayoutStateImpl.startTransition].
*/
internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
internal var previewTransformationSpec: TransformationSpecImpl? = null
+ internal var _cuj: Int? = null
/**
* 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/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index d1bd52b56ddd..f3be5e43c294 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -198,23 +198,30 @@ class SceneTransitionLayoutStateTest {
assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
// C => A. This should automatically call freezeAndAnimateToCurrentState() on bToC.
- state.startTransitionImmediately(animationScope = backgroundScope, cToA)
+ val cToAJob = state.startTransitionImmediately(animationScope = backgroundScope, cToA)
assertThat(frozenTransitions).containsExactly(aToB, bToC)
assertThat(state.finishedTransitions).isEmpty()
assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
- // Mark bToC as finished. The list of current transitions does not change because aToB is
- // still not marked as finished.
+ // Mark aToB and bToC as finished. The list of current transitions does not change because
+ // cToA is still running.
+ aToB.finish()
+ aToBJob.join()
+ assertThat(state.finishedTransitions).containsExactly(aToB)
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
bToC.finish()
bToCJob.join()
- assertThat(state.finishedTransitions).containsExactly(bToC)
+ assertThat(state.finishedTransitions).containsExactly(aToB, bToC)
assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
- // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions.
- aToB.finish()
- aToBJob.join()
+ // Mark cToA as finished. This should clear all transitions and settle to idle.
+ cToA.finish()
+ cToAJob.join()
assertThat(state.finishedTransitions).isEmpty()
- assertThat(state.currentTransitions).containsExactly(cToA).inOrder()
+ assertThat(state.currentTransitions).isEmpty()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
}
@Test
@@ -473,4 +480,77 @@ class SceneTransitionLayoutStateTest {
"SceneKey(debugName=SceneB)"
)
}
+
+ @Test
+ fun snapToScene_multipleTransitions() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ state.startTransitionImmediately(this, transition(SceneA, SceneB))
+ state.startTransitionImmediately(this, transition(SceneB, SceneC))
+ state.snapToScene(SceneC)
+
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneC)
+ }
+
+ @Test
+ fun trackTransitionCujs() = runTest {
+ val started = mutableSetOf<TransitionState.Transition>()
+ val finished = mutableSetOf<TransitionState.Transition>()
+ val cujWhenStarting = mutableMapOf<TransitionState.Transition, Int?>()
+ val state =
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ transitions {
+ // A <=> B.
+ from(SceneA, to = SceneB, cuj = 1)
+
+ // A <=> C.
+ from(SceneA, to = SceneC, cuj = 2)
+ from(SceneC, to = SceneA, cuj = 3)
+ },
+ onTransitionStart = { transition ->
+ started.add(transition)
+ cujWhenStarting[transition] = transition.cuj
+ },
+ onTransitionEnd = { finished.add(it) },
+ )
+
+ val aToB = transition(SceneA, SceneB)
+ val bToA = transition(SceneB, SceneA)
+ val aToC = transition(SceneA, SceneC)
+ val cToA = transition(SceneC, SceneA)
+
+ val animationScope = this
+ state.startTransitionImmediately(animationScope, aToB)
+ assertThat(started).containsExactly(aToB)
+ assertThat(finished).isEmpty()
+
+ state.startTransitionImmediately(animationScope, bToA)
+ assertThat(started).containsExactly(aToB, bToA)
+ assertThat(finished).isEmpty()
+
+ aToB.finish()
+ runCurrent()
+ assertThat(finished).containsExactly(aToB)
+
+ state.startTransitionImmediately(animationScope, aToC)
+ assertThat(started).containsExactly(aToB, bToA, aToC)
+ assertThat(finished).containsExactly(aToB)
+
+ state.startTransitionImmediately(animationScope, cToA)
+ assertThat(started).containsExactly(aToB, bToA, aToC, cToA)
+ assertThat(finished).containsExactly(aToB)
+
+ bToA.finish()
+ aToC.finish()
+ cToA.finish()
+ runCurrent()
+ assertThat(started).containsExactly(aToB, bToA, aToC, cToA)
+ assertThat(finished).containsExactly(aToB, bToA, aToC, cToA)
+
+ assertThat(cujWhenStarting[aToB]).isEqualTo(1)
+ assertThat(cujWhenStarting[bToA]).isEqualTo(1)
+ assertThat(cujWhenStarting[aToC]).isEqualTo(2)
+ assertThat(cujWhenStarting[cToA]).isEqualTo(3)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index fdbd0f63292a..7c8c6e5f6c12 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -379,8 +379,8 @@ class SceneTransitionLayoutTest {
assertThat(transition).hasProgress(0.5f)
rule.waitForIdle()
- // B and C are composed.
- rule.onNodeWithTag("aRoot").assertDoesNotExist()
+ // A, B and C are still composed given that B => C is not finished yet.
+ rule.onNodeWithTag("aRoot").assertExists()
rule.onNodeWithTag("bRoot").assertExists()
rule.onNodeWithTag("cRoot").assertExists()
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
deleted file mode 100644
index 15373d354ef6..000000000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
+++ /dev/null
@@ -1,282 +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.shared.clocks
-
-import android.graphics.Point
-import android.view.animation.Interpolator
-import com.android.app.animation.Interpolators
-import com.android.internal.annotations.Keep
-import com.android.systemui.monet.Style as MonetStyle
-import com.android.systemui.shared.clocks.view.HorizontalAlignment
-import com.android.systemui.shared.clocks.view.VerticalAlignment
-
-/** Data format for a simple asset-defined clock */
-@Keep
-data class ClockDesign(
- val id: String,
- val name: String? = null,
- val description: String? = null,
- val thumbnail: String? = null,
- val large: ClockFace? = null,
- val small: ClockFace? = null,
- @MonetStyle.Type val colorPalette: Int? = null,
-)
-
-/** Describes a clock using layers */
-@Keep
-data class ClockFace(
- val layers: List<ClockLayer> = listOf<ClockLayer>(),
- val layerBounds: LayerBounds = LayerBounds.FIT,
- val wallpaper: String? = null,
- val faceLayout: DigitalFaceLayout? = null,
- val pickerScale: ClockFaceScaleInPicker? = ClockFaceScaleInPicker(1.0f, 1.0f),
-)
-
-@Keep data class ClockFaceScaleInPicker(val scaleX: Float, val scaleY: Float)
-
-/** Base Type for a Clock Layer */
-@Keep
-interface ClockLayer {
- /** Override of face LayerBounds setting for this layer */
- val layerBounds: LayerBounds?
-}
-
-/** Clock layer that renders a static asset */
-@Keep
-data class AssetLayer(
- /** Asset to render in this layer */
- val asset: AssetReference,
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** Clock layer that renders the time (or a component of it) using numerals */
-@Keep
-data class DigitalHandLayer(
- /** See SimpleDateFormat for timespec format info */
- val timespec: DigitalTimespec,
- val style: TextStyle,
- // adoStyle concrete type must match style,
- // cause styles will transition between style and aodStyle
- val aodStyle: TextStyle?,
- val timer: Int? = null,
- override val layerBounds: LayerBounds? = null,
- var faceLayout: DigitalFaceLayout? = null,
- // we pass 12-hour format from json, which will be converted to 24-hour format in codes
- val dateTimeFormat: String,
- val alignment: DigitalAlignment?,
- // ratio of margins to measured size, currently used for handwritten clocks
- val marginRatio: DigitalMarginRatio? = DigitalMarginRatio(),
-) : ClockLayer
-
-/** Clock layer that renders the time (or a component of it) using numerals */
-@Keep
-data class ComposedDigitalHandLayer(
- val customizedView: String? = null,
- /** See SimpleDateFormat for timespec format info */
- val digitalLayers: List<DigitalHandLayer> = listOf<DigitalHandLayer>(),
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-@Keep
-data class DigitalAlignment(
- val horizontalAlignment: HorizontalAlignment?,
- val verticalAlignment: VerticalAlignment?,
-)
-
-@Keep
-data class DigitalMarginRatio(
- val left: Float = 0F,
- val top: Float = 0F,
- val right: Float = 0F,
- val bottom: Float = 0F,
-)
-
-/** Clock layer which renders a component of the time using an analog hand */
-@Keep
-data class AnalogHandLayer(
- val timespec: AnalogTimespec,
- val tickMode: AnalogTickMode,
- val asset: AssetReference,
- val timer: Int? = null,
- val clock_pivot: Point = Point(0, 0),
- val asset_pivot: Point? = null,
- val length: Float = 1f,
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** Clock layer which renders the time using an AVD */
-@Keep
-data class AnimatedHandLayer(
- val timespec: AnalogTimespec,
- val asset: AssetReference,
- val timer: Int? = null,
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** A collection of asset references for use in different device modes */
-@Keep
-data class AssetReference(
- val light: String,
- val dark: String,
- val doze: String? = null,
- val lightTint: String? = null,
- val darkTint: String? = null,
- val dozeTint: String? = null,
-)
-
-/**
- * Core TextStyling attributes for text clocks. Both color and sizing information can be applied to
- * either subtype.
- */
-@Keep
-interface TextStyle {
- // fontSizeScale is a scale factor applied to the default clock's font size.
- val fontSizeScale: Float?
-}
-
-/**
- * This specifies a font and styling parameters for that font. This is rendered using a text view
- * and the text animation classes used by the default clock. To ensure default value take effects,
- * all parameters MUST have a default value
- */
-@Keep
-data class FontTextStyle(
- // Font to load and use in the TextView
- val fontFamily: String? = null,
- val lineHeight: Float? = null,
- val borderWidth: String? = null,
- // ratio of borderWidth / fontSize
- val borderWidthScale: Float? = null,
- // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
- val fillColorLight: String? = null,
- // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
- val fillColorDark: String? = null,
- override val fontSizeScale: Float? = null,
- // used when alternate in one font file is needed
- var fontFeatureSettings: String? = null,
- val renderType: RenderType = RenderType.STROKE_TEXT,
- val outlineColor: String? = null,
- val transitionDuration: Long = -1L,
- val transitionInterpolator: InterpolatorEnum? = null,
-) : TextStyle
-
-/**
- * As an alternative to using a font, we can instead render a digital clock using a set of drawables
- * for each numeral, and optionally a colon. These drawables will be rendered directly after sizing
- * and placing them. This may be easier than generating a font file in some cases, and is provided
- * for ease of use. Unlike fonts, these are not localizable to other numeric systems (like Burmese).
- */
-@Keep
-data class LottieTextStyle(
- val numbers: List<String> = listOf(),
- // Spacing between numbers, dimension string
- val spacing: String = "0dp",
- // Colon drawable may be omitted if unused in format spec
- val colon: String? = null,
- // key is keypath name to get strokes from lottie, value is the color name to query color in
- // palette, e.g. @android:color/system_accent1_100
- val fillColorLightMap: Map<String, String>? = null,
- val fillColorDarkMap: Map<String, String>? = null,
- override val fontSizeScale: Float? = null,
- val paddingVertical: String = "0dp",
- val paddingHorizontal: String = "0dp",
-) : TextStyle
-
-/** Layer sizing mode for the clockface or layer */
-enum class LayerBounds {
- /**
- * Sized so the larger dimension matches the allocated space. This results in some of the
- * allocated space being unused.
- */
- FIT,
-
- /**
- * Sized so the smaller dimension matches the allocated space. This will clip some content to
- * the edges of the space.
- */
- FILL,
-
- /** Fills the allocated space exactly by stretching the layer */
- STRETCH,
-}
-
-/** Ticking mode for analog hands. */
-enum class AnalogTickMode {
- SWEEP,
- TICK,
-}
-
-/** Timspec options for Analog Hands. Named for tick interval. */
-enum class AnalogTimespec {
- SECONDS,
- MINUTES,
- HOURS,
- HOURS_OF_DAY,
- DAY_OF_WEEK,
- DAY_OF_MONTH,
- DAY_OF_YEAR,
- WEEK,
- MONTH,
- TIMER,
-}
-
-enum class DigitalTimespec {
- TIME_FULL_FORMAT,
- DIGIT_PAIR,
- FIRST_DIGIT,
- SECOND_DIGIT,
- DATE_FORMAT,
-}
-
-enum class DigitalFaceLayout {
- // can only use HH_PAIR, MM_PAIR from DigitalTimespec
- TWO_PAIRS_VERTICAL,
- TWO_PAIRS_HORIZONTAL,
- // can only use HOUR_FIRST_DIGIT, HOUR_SECOND_DIGIT, MINUTE_FIRST_DIGIT, MINUTE_SECOND_DIGIT
- // from DigitalTimespec, used for tabular layout when the font doesn't support tnum
- FOUR_DIGITS_ALIGN_CENTER,
- FOUR_DIGITS_HORIZONTAL,
-}
-
-enum class RenderType {
- CHANGE_WEIGHT,
- HOLLOW_TEXT,
- STROKE_TEXT,
- OUTER_OUTLINE_TEXT,
-}
-
-enum class InterpolatorEnum(factory: () -> Interpolator) {
- STANDARD({ Interpolators.STANDARD }),
- EMPHASIZED({ Interpolators.EMPHASIZED });
-
- val interpolator: Interpolator by lazy(factory)
-}
-
-fun generateDigitalLayerIdString(layer: DigitalHandLayer): String {
- return if (
- layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
- layer.timespec == DigitalTimespec.DATE_FORMAT
- ) {
- layer.timespec.toString()
- } else {
- if ("h" in layer.dateTimeFormat) {
- "HOUR" + "_" + layer.timespec.toString()
- } else {
- "MINUTE" + "_" + layer.timespec.toString()
- }
- }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index d0a32dcf9865..9fb60c75b046 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -18,6 +18,7 @@ package com.android.systemui.shared.clocks
import android.graphics.Rect
import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
import com.android.systemui.log.core.Logger
import com.android.systemui.plugins.clocks.AlarmData
import com.android.systemui.plugins.clocks.ClockAnimations
@@ -29,14 +30,13 @@ import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
import java.util.Locale
import java.util.TimeZone
-class ComposedDigitalLayerController(
- private val clockCtx: ClockContext,
- private val layer: ComposedDigitalHandLayer,
-) : SimpleClockLayerController {
+class ComposedDigitalLayerController(private val clockCtx: ClockContext) :
+ SimpleClockLayerController {
private val logger =
Logger(clockCtx.messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
@@ -46,14 +46,40 @@ class ComposedDigitalLayerController(
override val view = FlexClockView(clockCtx)
init {
- layer.digitalLayers.forEach {
- val childView = SimpleDigitalClockTextView(clockCtx)
- val controller =
- SimpleDigitalHandLayerController(clockCtx, it as DigitalHandLayer, childView)
-
- view.addView(childView)
+ fun createController(cfg: LayerConfig) {
+ val controller = SimpleDigitalHandLayerController(clockCtx, cfg)
+ view.addView(controller.view)
layerControllers.add(controller)
}
+
+ val layerCfg =
+ LayerConfig(
+ style = FontTextStyle(lineHeight = 147.25f),
+ aodStyle =
+ FontTextStyle(
+ transitionInterpolator = Interpolators.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.BASELINE),
+
+ // Placeholders
+ timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ dateTimeFormat = "hh:mm",
+ )
+
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "hh")
+ )
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "hh")
+ )
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "mm")
+ )
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "mm")
+ )
}
private fun refreshTime() {
@@ -79,17 +105,11 @@ class ComposedDigitalLayerController(
refreshTime()
}
- override fun onWeatherDataChanged(data: WeatherData) {
- view.onWeatherDataChanged(data)
- }
+ override fun onWeatherDataChanged(data: WeatherData) {}
- override fun onAlarmDataChanged(data: AlarmData) {
- view.onAlarmDataChanged(data)
- }
+ override fun onAlarmDataChanged(data: AlarmData) {}
- override fun onZenDataChanged(data: ZenData) {
- view.onZenDataChanged(data)
- }
+ override fun onZenDataChanged(data: ZenData) {}
override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
view.updateAxes(axes)
@@ -123,15 +143,11 @@ class ComposedDigitalLayerController(
view.animateCharge()
}
- override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
- view.onPositionUpdated(fromLeft, direction, fraction)
- }
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
override fun onPositionUpdated(distance: Float, fraction: Float) {}
- override fun onPickerCarouselSwiping(swipingFraction: Float) {
- view.onPickerCarouselSwiping(swipingFraction)
- }
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {}
}
override val faceEvents =
@@ -163,9 +179,8 @@ class ComposedDigitalLayerController(
override val config =
ClockFaceConfig(
- hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay,
- hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation,
- useCustomClockScene = view.useCustomClockScene,
+ hasCustomWeatherDataDisplay = false,
+ hasCustomPositionUpdatedAnimation = true,
)
@VisibleForTesting
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 c73e1c33f88a..f6ff3268fca4 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
@@ -27,8 +27,6 @@ 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"
@@ -78,8 +76,7 @@ class DefaultClockProvider(
typefaceCache,
buffers,
buffers.infraMessageBuffer,
- ),
- FLEX_DESIGN,
+ )
)
} else {
DefaultClockController(ctx, layoutInflater, resources, settings, messageBuffers)
@@ -128,119 +125,5 @@ class DefaultClockProvider(
// TODO(b/364680873): Move constant to config_clockFontFamily when shipping
Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
}
-
- 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(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "hh",
- ),
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.SECOND_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "hh",
- ),
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.FIRST_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "mm",
- ),
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.SECOND_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "mm",
- ),
- ),
- )
- )
-
- val smallLayer =
- listOf(
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.TIME_FULL_FORMAT,
- style = FontTextStyle(fontSizeScale = 0.98f),
- aodStyle =
- FontTextStyle(
- 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/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index 7f01fd7c87ac..aed3a2df0436 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -32,21 +32,16 @@ import java.util.Locale
import java.util.TimeZone
/** Controller for the default flex clock */
-class FlexClockController(
- private val clockCtx: ClockContext,
- val design: ClockDesign, // TODO(b/364680879): Remove when done inlining
-) : ClockController {
+class FlexClockController(private val clockCtx: ClockContext) : ClockController {
override val smallClock =
FlexClockFaceController(
clockCtx.copy(messageBuffer = clockCtx.messageBuffers.smallClockMessageBuffer),
- design.small ?: design.large!!,
isLargeClock = false,
)
override val largeClock =
FlexClockFaceController(
clockCtx.copy(messageBuffer = clockCtx.messageBuffers.largeClockMessageBuffer),
- design.large ?: design.small!!,
isLargeClock = true,
)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index 4a47f1bc12bf..827bd6898310 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -35,17 +35,14 @@ import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
import java.util.Locale
import java.util.TimeZone
import kotlin.math.max
// TODO(b/364680879): Merge w/ ComposedDigitalLayerController
-class FlexClockFaceController(
- clockCtx: ClockContext,
- face: ClockFace,
- private val isLargeClock: Boolean,
-) : ClockFaceController {
+class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: Boolean) :
+ ClockFaceController {
override val view: View
get() = layerController.view
@@ -59,19 +56,12 @@ class FlexClockFaceController(
val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
init {
- val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- lp.gravity = Gravity.CENTER
-
- val layer = face.layers[0]
-
layerController =
- if (isLargeClock) {
- ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer)
- } else {
- val childView = SimpleDigitalClockTextView(clockCtx)
- SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView)
- }
- layerController.view.layoutParams = lp
+ if (isLargeClock) ComposedDigitalLayerController(clockCtx)
+ else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG)
+
+ layerController.view.layoutParams =
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply { gravity = Gravity.CENTER }
}
/** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */
@@ -227,10 +217,6 @@ class FlexClockFaceController(
}
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 (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) {
view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction
}
@@ -248,4 +234,15 @@ class FlexClockFaceController(
// TODO(b/378128811) port stepping animation
}
}
+
+ companion object {
+ val SMALL_LAYER_CONFIG =
+ LayerConfig(
+ timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ style = FontTextStyle(fontSizeScale = 0.98f),
+ aodStyle = FontTextStyle(),
+ alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+ dateTimeFormat = "h:mm",
+ )
+ }
}
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
deleted file mode 100644
index 6e1b9aabf86d..000000000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-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
index ebac4b24732b..82fc35012dbc 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -17,8 +17,8 @@
package com.android.systemui.shared.clocks
import android.graphics.Rect
-import android.view.View
import android.view.ViewGroup
+import android.view.animation.Interpolator
import android.widget.RelativeLayout
import androidx.annotation.VisibleForTesting
import com.android.systemui.customization.R
@@ -32,22 +32,56 @@ import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.VerticalAlignment
import java.util.Locale
import java.util.TimeZone
private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
-open class SimpleDigitalHandLayerController<T>(
+// TODO(b/364680879): The remains of ClockDesign. Cut further.
+data class LayerConfig(
+ val style: FontTextStyle,
+ val aodStyle: FontTextStyle,
+ val alignment: DigitalAlignment,
+ val timespec: DigitalTimespec,
+ val dateTimeFormat: String,
+) {
+ fun generateDigitalLayerIdString(): String {
+ return when {
+ timespec == DigitalTimespec.TIME_FULL_FORMAT -> "$timespec"
+ "h" in dateTimeFormat -> "HOUR_$timespec"
+ else -> "MINUTE_$timespec"
+ }
+ }
+}
+
+data class DigitalAlignment(
+ val horizontalAlignment: HorizontalAlignment?,
+ val verticalAlignment: VerticalAlignment?,
+)
+
+data class FontTextStyle(
+ val lineHeight: Float? = null,
+ val fontSizeScale: Float? = null,
+ val transitionDuration: Long = -1L,
+ val transitionInterpolator: Interpolator? = null,
+)
+
+enum class DigitalTimespec {
+ TIME_FULL_FORMAT,
+ FIRST_DIGIT,
+ SECOND_DIGIT,
+}
+
+open class SimpleDigitalHandLayerController(
private val clockCtx: ClockContext,
- private val layer: DigitalHandLayer,
- override val view: T,
-) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
+ private val layerCfg: LayerConfig,
+) : SimpleClockLayerController {
+ override val view = SimpleDigitalClockTextView(clockCtx)
private val logger = Logger(clockCtx.messageBuffer, TAG)
- val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
-
- @VisibleForTesting
- fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
+ val timespec = DigitalTimespecHandler(layerCfg.timespec, layerCfg.dateTimeFormat)
@VisibleForTesting
override var fakeTimeMills: Long?
@@ -65,145 +99,17 @@ open class SimpleDigitalHandLayerController<T>(
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(layer.style, layer.aodStyle)
+ layerCfg.alignment.verticalAlignment?.let { view.verticalAlignment = it }
+ layerCfg.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
+ view.applyStyles(layerCfg.style, layerCfg.aodStyle)
view.id =
clockCtx.resources.getIdentifier(
- generateDigitalLayerIdString(layer),
+ layerCfg.generateDigitalLayerIdString(),
"id",
clockCtx.context.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()
@@ -248,7 +154,6 @@ open class SimpleDigitalHandLayerController<T>(
override val animations =
object : ClockAnimations {
override fun enter() {
- applyLayout(layer.faceLayout)
refreshTime()
}
@@ -264,7 +169,6 @@ open class SimpleDigitalHandLayerController<T>(
}
override fun fold(fraction: Float) {
- applyLayout(layer.faceLayout)
refreshTime()
}
@@ -283,17 +187,13 @@ open class SimpleDigitalHandLayerController<T>(
object : ClockFaceEvents {
override fun onTimeTick() {
refreshTime()
- if (
- layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
- layer.timespec == DigitalTimespec.DATE_FORMAT
- ) {
+ if (layerCfg.timespec == DigitalTimespec.TIME_FULL_FORMAT) {
view.contentDescription = timespec.getContentDescription()
}
}
override fun onFontSettingChanged(fontSizePx: Float) {
view.applyTextSize(fontSizePx)
- applyMargin()
}
override fun onThemeChanged(theme: ThemeConfig) {
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
index ed6a403a7c7e..37db783aba53 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -25,9 +25,7 @@ import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
-open class TimespecHandler(
- val cal: Calendar,
-) {
+open class TimespecHandler(val cal: Calendar) {
var timeZone: TimeZone
get() = cal.timeZone
set(value) {
@@ -82,10 +80,7 @@ class DigitalTimespecHandler(
}
private fun updateSimpleDateFormat(locale: Locale): DateFormat {
- if (
- locale.language.equals(Locale.ENGLISH.language) ||
- timespec != DigitalTimespec.DATE_FORMAT
- ) {
+ if (locale.language.equals(Locale.ENGLISH.language)) {
// force date format in English, and time format to use format defined in json
return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
} else {
@@ -97,24 +92,18 @@ class DigitalTimespecHandler(
return when (timespec) {
DigitalTimespec.TIME_FULL_FORMAT ->
SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
- DigitalTimespec.DATE_FORMAT ->
- SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
- else -> {
- null
- }
+ 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
- )
- }
+ (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 {
@@ -122,7 +111,7 @@ class DigitalTimespecHandler(
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
+ if (isFirstDigit) text.length - 1 else text.length,
)
}
@@ -130,27 +119,16 @@ class DigitalTimespecHandler(
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()
- }
+ DigitalTimespec.TIME_FULL_FORMAT -> dateFormat.format(cal.time).toString()
}
}
fun getContentDescription(): String? {
return when (timespec) {
- DigitalTimespec.TIME_FULL_FORMAT,
- DigitalTimespec.DATE_FORMAT -> {
+ DigitalTimespec.TIME_FULL_FORMAT -> {
contentDescriptionFormat?.format(cal.time).toString()
}
- else -> {
- return null
- }
+ else -> return null
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
deleted file mode 100644
index d4eb7677c757..000000000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
+++ /dev/null
@@ -1,184 +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.shared.clocks.view
-
-import android.graphics.Canvas
-import android.graphics.Point
-import android.view.View
-import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.log.core.Logger
-import com.android.systemui.plugins.clocks.AlarmData
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.plugins.clocks.WeatherData
-import com.android.systemui.plugins.clocks.ZenData
-import com.android.systemui.shared.clocks.ClockContext
-import com.android.systemui.shared.clocks.LogUtil
-import java.util.Locale
-
-// TODO(b/364680879): Merge w/ only subclass FlexClockView
-abstract class DigitalClockFaceView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
- protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
- get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
-
- abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
-
- @VisibleForTesting
- var isAnimationEnabled = true
- set(value) {
- field = value
- digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
- }
-
- var dozeFraction: Float = 0F
- set(value) {
- field = value
- digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
- }
-
- val dozeControlState = DozeControlState()
-
- var isReactiveTouchInteractionEnabled = false
- set(value) {
- field = value
- }
-
- open val text: String?
- get() = null
-
- open fun refreshTime() = logger.d("refreshTime()")
-
- override fun invalidate() {
- logger.d("invalidate()")
- super.invalidate()
- }
-
- override fun requestLayout() {
- logger.d("requestLayout()")
- super.requestLayout()
- }
-
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- logger.d("onMeasure()")
- calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { setMeasuredDimension(it.x, it.y) }
- ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
- calculateLeftTopPosition()
- dozeControlState.animateReady = true
- }
-
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- logger.d("onLayout()")
- super.onLayout(changed, left, top, right, bottom)
- }
-
- override fun onDraw(canvas: Canvas) {
- text?.let { logger.d({ "onDraw($str1)" }) { str1 = it } } ?: run { logger.d("onDraw()") }
- super.onDraw(canvas)
- }
-
- /*
- * Called in onMeasure to generate width/height overrides to the normal measuring logic. A null
- * result causes the normal view measuring logic to execute.
- */
- protected open fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? = null
-
- protected open fun calculateLeftTopPosition() {}
-
- override fun addView(child: View?) {
- if (child == null) return
- logger.d({ "addView($str1 @$int1)" }) {
- str1 = child::class.simpleName!!
- int1 = child.id
- }
- super.addView(child)
- if (child is SimpleDigitalClockTextView) {
- digitalClockTextViewMap[child.id] = child
- }
- child.setWillNotDraw(true)
- }
-
- open fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
- digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
- }
-
- open fun animateCharge() {
- digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
- }
-
- open fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
-
- fun updateColor(color: Int) {
- digitalClockTextViewMap.forEach { _, view -> view.updateColor(color) }
- invalidate()
- }
-
- fun updateAxes(axes: List<ClockFontAxisSetting>) {
- digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
- requestLayout()
- }
-
- fun onFontSettingChanged(fontSizePx: Float) {
- digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
- }
-
- open val hasCustomWeatherDataDisplay
- get() = false
-
- open val hasCustomPositionUpdatedAnimation
- get() = false
-
- /** True if it's large weather clock, will use weatherBlueprint in compose */
- open val useCustomClockScene
- get() = false
-
- open fun onLocaleChanged(locale: Locale) {}
-
- open fun onWeatherDataChanged(data: WeatherData) {}
-
- open fun onAlarmDataChanged(data: AlarmData) {}
-
- open fun onZenDataChanged(data: ZenData) {}
-
- open fun onPickerCarouselSwiping(swipingFraction: Float) {}
-
- open fun isAlignedWithScreen(): Boolean = false
-
- /**
- * animateDoze needs correct translate value, which is calculated in onMeasure so we need to
- * delay this animation when we get correct values
- */
- class DozeControlState {
- var animateDoze: () -> Unit = {}
- set(value) {
- if (animateReady) {
- value()
- field = {}
- } else {
- field = value
- }
- }
-
- var animateReady = false
- set(value) {
- if (value) {
- animateDoze()
- animateDoze = {}
- }
- field = value
- }
- }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index faef18c1e85a..c40bb9a5ebea 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -22,11 +22,16 @@ import android.icu.text.NumberFormat
import android.util.MathUtils.constrainedMap
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.RelativeLayout
+import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators
import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.shared.clocks.ClockContext
import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.LogUtil
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
@@ -34,14 +39,38 @@ import kotlin.math.min
fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
-class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
- override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
+class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
+ protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
+ get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+ @VisibleForTesting
+ var isAnimationEnabled = true
+ set(value) {
+ field = value
+ digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
+ }
+
+ var dozeFraction: Float = 0F
+ set(value) {
+ field = value
+ digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
+ }
+
+ var isReactiveTouchInteractionEnabled = false
+ set(value) {
+ field = value
+ }
+
+ var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
private val digitLeftTopMap = mutableMapOf<Int, Point>()
private var maxSingleDigitSize = Point(-1, -1)
private val lockscreenTranslate = Point(0, 0)
private var aodTranslate = Point(0, 0)
+ private var onAnimateDoze: (() -> Unit)? = null
+ private var isDozeReadyToAnimate = false
+
// Does the current language have mono vertical size when displaying numerals
private var isMonoVerticalNumericLineSpacing = true
@@ -57,13 +86,7 @@ class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
private val digitOffsets = mutableMapOf<Int, Float>()
- override fun addView(child: View?) {
- super.addView(child)
- (child as SimpleDigitalClockTextView).digitTranslateAnimator =
- DigitTranslateAnimator(::invalidate)
- }
-
- protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+ protected fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? {
maxSingleDigitSize = Point(-1, -1)
val bottomLocation: (textView: SimpleDigitalClockTextView) -> Int = { textView ->
if (isMonoVerticalNumericLineSpacing) {
@@ -85,7 +108,7 @@ class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
)
}
- protected override fun calculateLeftTopPosition() {
+ protected fun calculateLeftTopPosition() {
digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
@@ -96,13 +119,57 @@ class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
}
}
- override fun refreshTime() {
- super.refreshTime()
+ override fun addView(child: View?) {
+ if (child == null) return
+ logger.d({ "addView($str1 @$int1)" }) {
+ str1 = child::class.simpleName!!
+ int1 = child.id
+ }
+
+ super.addView(child)
+ (child as? SimpleDigitalClockTextView)?.let {
+ it.digitTranslateAnimator = DigitTranslateAnimator(::invalidate)
+ digitalClockTextViewMap[child.id] = child
+ }
+ child.setWillNotDraw(true)
+ }
+
+ fun refreshTime() {
+ logger.d("refreshTime()")
digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
}
+ override fun invalidate() {
+ logger.d("invalidate()")
+ super.invalidate()
+ }
+
+ override fun requestLayout() {
+ logger.d("requestLayout()")
+ super.requestLayout()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ logger.d("onMeasure()")
+ calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { size ->
+ setMeasuredDimension(size.x, size.y)
+ } ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
+ calculateLeftTopPosition()
+
+ isDozeReadyToAnimate = true
+ onAnimateDoze?.invoke()
+ onAnimateDoze = null
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ logger.d("onLayout()")
+ super.onLayout(changed, left, top, right, bottom)
+ }
+
override fun onDraw(canvas: Canvas) {
+ logger.d("onDraw()")
super.onDraw(canvas)
+
digitalClockTextViewMap.forEach { (id, textView) ->
// save canvas location in anticipation of restoration later
canvas.save()
@@ -117,14 +184,30 @@ class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
}
}
- override fun onLocaleChanged(locale: Locale) {
+ fun isAlignedWithScreen(): Boolean = false
+
+ fun onLocaleChanged(locale: Locale) {
updateLocale(locale)
requestLayout()
}
- override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
- dozeControlState.animateDoze = {
- super.animateDoze(isDozing, isAnimated)
+ fun updateColor(color: Int) {
+ digitalClockTextViewMap.forEach { _, view -> view.updateColor(color) }
+ invalidate()
+ }
+
+ fun updateAxes(axes: List<ClockFontAxisSetting>) {
+ digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
+ requestLayout()
+ }
+
+ fun onFontSettingChanged(fontSizePx: Float) {
+ digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
+ }
+
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ fun executeDozeAnimation() {
+ digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
if (maxSingleDigitSize.x < 0 || maxSingleDigitSize.y < 0) {
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
}
@@ -150,10 +233,13 @@ class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
}
}
}
+
+ if (isDozeReadyToAnimate) executeDozeAnimation()
+ else onAnimateDoze = { executeDozeAnimation() }
}
- override fun animateCharge() {
- super.animateCharge()
+ fun animateCharge() {
+ digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
digitalClockTextViewMap.forEach { (id, textView) ->
textView.digitTranslateAnimator?.let {
it.animatePosition(
@@ -301,7 +387,7 @@ class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
// Add language tags below that do not have vertically mono spaced numerals
private val NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES =
setOf(
- "my", // Burmese
+ "my" // Burmese
)
// Use the sign of targetTranslation to control the direction of digit translation
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index cef24e99084e..0f8ca947479b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -44,19 +44,30 @@ import com.android.systemui.shared.clocks.DigitTranslateAnimator
import com.android.systemui.shared.clocks.DimensionParser
import com.android.systemui.shared.clocks.FontTextStyle
import com.android.systemui.shared.clocks.LogUtil
-import com.android.systemui.shared.clocks.RenderType
-import com.android.systemui.shared.clocks.TextStyle
import java.lang.Thread
import kotlin.math.max
import kotlin.math.min
private val TAG = SimpleDigitalClockTextView::class.simpleName!!
+enum class VerticalAlignment {
+ TOP,
+ BOTTOM,
+ BASELINE, // default
+ CENTER,
+}
+
+enum class HorizontalAlignment {
+ LEFT,
+ RIGHT,
+ CENTER, // default
+}
+
@SuppressLint("AppCompatCustomView")
open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) :
- TextView(clockCtx.context, attrs), SimpleDigitalClockView {
+ TextView(clockCtx.context, attrs) {
val lockScreenPaint = TextPaint()
- override lateinit var textStyle: FontTextStyle
+ lateinit var textStyle: FontTextStyle
lateinit var aodStyle: FontTextStyle
private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION)
@@ -98,25 +109,20 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
TextAnimator(layout, typefaceCache, invalidateCb)
}
- override var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
- override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
- override var isAnimationEnabled = true
- override var dozeFraction: Float = 0F
+ var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
+ var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
+ var isAnimationEnabled = true
+ var dozeFraction: Float = 0F
set(value) {
field = value
invalidate()
}
- // Have to passthrough to unify View with SimpleDigitalClockView
- override var text: String
- get() = super.getText().toString()
- set(value) = super.setText(value)
-
var textBorderWidth = 0F
var baselineFromMeasure = 0
var lockscreenColor = Color.WHITE
- override fun updateColor(color: Int) {
+ fun updateColor(color: Int) {
lockscreenColor = color
lockScreenPaint.color = lockscreenColor
if (dozeFraction < 1f) {
@@ -125,7 +131,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
invalidate()
}
- override fun updateAxes(axes: List<ClockFontAxisSetting>) {
+ fun updateAxes(axes: List<ClockFontAxisSetting>) {
lsFontVariation = ClockFontAxisSetting.toFVar(axes + OPTICAL_SIZE_AXIS)
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
@@ -226,24 +232,6 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
canvas.translate(it.updatedTranslate.x.toFloat(), it.updatedTranslate.y.toFloat())
}
- if (aodStyle.renderType == RenderType.HOLLOW_TEXT) {
- canvas.saveLayer(
- -translation.x.toFloat(),
- -translation.y.toFloat(),
- (-translation.x + measuredWidth).toFloat(),
- (-translation.y + measuredHeight).toFloat(),
- null,
- )
- canvas.saveLayer(
- -translation.x.toFloat(),
- -translation.y.toFloat(),
- (-translation.x + measuredWidth).toFloat(),
- (-translation.y + measuredHeight).toFloat(),
- PORTER_DUFF_XFER_MODE_PAINT,
- )
- canvas.restore()
- canvas.restore()
- }
textAnimator.draw(canvas)
digitTranslateAnimator?.let {
@@ -258,15 +246,15 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
override fun invalidate() {
logger.d("invalidate()")
super.invalidate()
- (parent as? DigitalClockFaceView)?.invalidate()
+ (parent as? FlexClockView)?.invalidate()
}
- override fun refreshTime() {
+ fun refreshTime() {
logger.d("refreshTime()")
refreshText()
}
- override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
if (!this::textAnimator.isInitialized) return
textAnimator.setTextStyle(
animate = isAnimated && isAnimationEnabled,
@@ -279,7 +267,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
updateTextBoundsForTextAnimator()
}
- override fun animateCharge() {
+ fun animateCharge() {
if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
// Skip charge animation if dozing animation is already playing.
return
@@ -419,27 +407,15 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
return updateXtranslation(localTranslation, interpolatedTextBounds)
}
- override fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) {
- this.textStyle = textStyle as FontTextStyle
- val typefaceName = "fonts/" + textStyle.fontFamily
+ fun applyStyles(textStyle: FontTextStyle, aodStyle: FontTextStyle?) {
+ this.textStyle = textStyle
lockScreenPaint.strokeJoin = Paint.Join.ROUND
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
- textStyle.fontFeatureSettings?.let {
- lockScreenPaint.fontFeatureSettings = it
- fontFeatureSettings = it
- }
typeface = lockScreenPaint.typeface
textStyle.lineHeight?.let { lineHeight = it.toInt() }
- // borderWidth in textStyle and aodStyle is used to draw,
- // strokeWidth in lockScreenPaint is used to measure and get enough space for the text
- textStyle.borderWidth?.let { textBorderWidth = parser.convert(it) }
- if (aodStyle != null && aodStyle is FontTextStyle) {
- this.aodStyle = aodStyle
- } else {
- this.aodStyle = textStyle.copy()
- }
- this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it.interpolator }
+ this.aodStyle = aodStyle ?: textStyle.copy()
+ this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it }
lockScreenPaint.strokeWidth = textBorderWidth
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
setInterpolatorPaint()
@@ -448,7 +424,7 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
}
// When constrainedByHeight is on, targetFontSizePx is the constrained height of textView
- override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) {
+ fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false) {
val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
aodFontSizePx =
@@ -463,7 +439,6 @@ open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSe
val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
}
- textStyle.borderWidthScale?.let { textBorderWidth = fontSizePx * it }
lockScreenPaint.strokeWidth = textBorderWidth
recomputeMaxSingleDigitSizes()
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
deleted file mode 100644
index e8be28fb8df2..000000000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
+++ /dev/null
@@ -1,57 +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.shared.clocks.view
-
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.TextStyle
-
-interface SimpleDigitalClockView {
- var text: String
- var verticalAlignment: VerticalAlignment
- var horizontalAlignment: HorizontalAlignment
- var dozeFraction: Float
- val textStyle: TextStyle
- @VisibleForTesting var isAnimationEnabled: Boolean
-
- fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?)
-
- fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
-
- fun updateColor(color: Int)
-
- fun updateAxes(axes: List<ClockFontAxisSetting>)
-
- fun refreshTime()
-
- fun animateCharge()
-
- fun animateDoze(isDozing: Boolean, isAnimated: Boolean)
-}
-
-enum class VerticalAlignment {
- TOP,
- BOTTOM,
- BASELINE, // default
- CENTER,
-}
-
-enum class HorizontalAlignment {
- LEFT,
- RIGHT,
- CENTER, // default
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
index e659ef274980..698fac107a1d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
@@ -18,7 +18,9 @@ package com.android.systemui.keyboard.shortcut.data.repository
import android.content.Context
import android.content.Context.INPUT_SERVICE
+import android.content.Intent
import android.hardware.input.InputGestureData
+import android.hardware.input.InputManager
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.fakeInputManager
import android.platform.test.annotations.EnableFlags
@@ -27,9 +29,12 @@ import androidx.test.filters.SmallTest
import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.customInputGesturesRepository
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.userTracker
@@ -48,18 +53,41 @@ import org.mockito.kotlin.whenever
@EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
class CustomInputGesturesRepositoryTest : SysuiTestCase() {
- private val mockUserContext: Context = mock()
+ private val primaryUserContext: Context = mock()
+ private val secondaryUserContext: Context = mock()
+ private var activeUserContext: Context = primaryUserContext
+
private val kosmos = testKosmos().also {
- it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
+ it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext })
}
private val inputManager = kosmos.fakeInputManager.inputManager
+ private val broadcastDispatcher = kosmos.broadcastDispatcher
+ private val inputManagerForSecondaryUser: InputManager = mock()
private val testScope = kosmos.testScope
+ private val testHelper = kosmos.shortcutHelperTestHelper
private val customInputGesturesRepository = kosmos.customInputGesturesRepository
@Before
- fun setup(){
- whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+ fun setup() {
+ activeUserContext = primaryUserContext
+ whenever(primaryUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+ whenever(secondaryUserContext.getSystemService(INPUT_SERVICE))
+ .thenReturn(inputManagerForSecondaryUser)
+ }
+
+ @Test
+ fun customInputGestures_emitsNewUsersInputGesturesWhenUserIsSwitch() {
+ testScope.runTest {
+ setCustomInputGesturesForPrimaryUser(allAppsInputGestureData)
+ setCustomInputGesturesForSecondaryUser(goHomeInputGestureData)
+
+ val inputGestures by collectLastValue(customInputGesturesRepository.customInputGestures)
+ assertThat(inputGestures).containsExactly(allAppsInputGestureData)
+
+ switchToSecondaryUser()
+ assertThat(inputGestures).containsExactly(goHomeInputGestureData)
+ }
}
@Test
@@ -115,4 +143,24 @@ class CustomInputGesturesRepositoryTest : SysuiTestCase() {
}
}
+ private fun setCustomInputGesturesForPrimaryUser(vararg inputGesture: InputGestureData) {
+ whenever(
+ inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+ ).thenReturn(inputGesture.toList())
+ }
+
+ private fun setCustomInputGesturesForSecondaryUser(vararg inputGesture: InputGestureData) {
+ whenever(
+ inputManagerForSecondaryUser.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+ ).thenReturn(inputGesture.toList())
+ }
+
+ private fun switchToSecondaryUser() {
+ activeUserContext = secondaryUserContext
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_SWITCHED)
+ )
+ }
+
} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index b29a5f4e456f..9e8713be3f5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -34,6 +34,7 @@ import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.data.repository.FlingInfo
import com.android.systemui.shade.data.repository.fakeShadeRepository
@@ -47,7 +48,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
-import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -55,9 +55,8 @@ import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject
class FromLockscreenTransitionInteractorTest : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
- this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository(
- testScope = testScope,
- ))
+ this.fakeKeyguardTransitionRepository =
+ spy(FakeKeyguardTransitionRepository(testScope = testScope))
}
private val testScope = kosmos.testScope
@@ -181,6 +180,12 @@ class FromLockscreenTransitionInteractorTest : SysuiTestCase() {
underTest.start()
assertThatRepository(transitionRepository).noTransitionsStarted()
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = testScope,
+ )
+
keyguardRepository.setKeyguardDismissible(true)
runCurrent()
shadeRepository.setCurrentFling(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
index ea2b3cdcf98c..605a5d261424 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
@@ -21,6 +21,7 @@ import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.IRemoteAnimationFinishedCallback
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -38,10 +39,13 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -222,4 +226,22 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() {
underTest.setSurfaceBehindVisibility(false)
verify(keyguardTransitions).startKeyguardTransition(eq(true), any())
}
+
+ @Test
+ fun remoteAnimationInstantlyFinished_ifDismissTransitionNotStarted() {
+ val mockedCallback = mock<IRemoteAnimationFinishedCallback>()
+ whenever(keyguardDismissTransitionInteractor.startDismissKeyguardTransition(any()))
+ .thenReturn(false)
+
+ underTest.onKeyguardGoingAwayRemoteAnimationStart(
+ transit = 0,
+ apps = emptyArray(),
+ wallpapers = emptyArray(),
+ nonApps = emptyArray(),
+ finishedCallback = mockedCallback,
+ )
+
+ verify(mockedCallback).onAnimationFinished()
+ verifyNoMoreInteractions(mockedCallback)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index a36e0eac086e..9bae7bd72f7d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -1,6 +1,7 @@
package com.android.systemui.navigationbar
import android.app.ActivityManager
+import android.os.Handler
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -61,6 +62,7 @@ class TaskbarDelegateTest : SysuiTestCase() {
@Mock lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock lateinit var mStatusBarStateController: StatusBarStateController
@Mock lateinit var mDisplayTracker: DisplayTracker
+ @Mock lateinit var mHandler: Handler
@Before
fun setup() {
@@ -69,6 +71,11 @@ class TaskbarDelegateTest : SysuiTestCase() {
`when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController)
`when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState)
`when`(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState)
+ `when`(mHandler.post(any())).thenAnswer {
+ (it.arguments[0] as Runnable).run()
+ true
+ }
+
mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
mTaskbarDelegate =
TaskbarDelegate(
@@ -76,6 +83,7 @@ class TaskbarDelegateTest : SysuiTestCase() {
mLightBarControllerFactory,
mStatusBarKeyguardViewManager,
mStatusBarStateController,
+ mHandler,
)
mTaskbarDelegate.setDependencies(
mCommandQueue,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index b5043ce700f1..fe44c3e31e66 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -39,7 +39,7 @@ class ShadeRepositoryImplTest : SysuiTestCase() {
@Before
fun setUp() {
- underTest = ShadeRepositoryImpl()
+ underTest = ShadeRepositoryImpl(testScope)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 4a3be4487290..20474c842b51 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -81,6 +81,7 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.DisableSceneContainer;
import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.log.LogWtfHandlerRule;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
@@ -90,6 +91,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -115,6 +117,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
@@ -169,6 +172,10 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
@Mock
private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
@Mock
+ private Lazy<KeyguardInteractor> mKeyguardInteractorLazy;
+ @Mock
+ private KeyguardInteractor mKeyguardInteractor;
+ @Mock
private StateFlow<DeviceUnlockStatus> mDeviceUnlockStatusStateFlow;
private UserInfo mCurrentUser;
@@ -181,6 +188,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
private NotificationEntry mSecondaryUserNotif;
private NotificationEntry mWorkProfileNotif;
private NotificationEntry mSensitiveContentNotif;
+ private long mSensitiveNotifPostTime;
private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
@@ -246,13 +254,17 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mSensitiveContentNotif = new NotificationEntryBuilder()
.setNotification(notifWithPrivateVisibility)
.setUser(new UserHandle(mCurrentUser.id))
+ .setPostTime(System.currentTimeMillis())
.build();
mSensitiveContentNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking())
.setChannel(channel)
.setSensitiveContent(true)
.setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
+ mSensitiveNotifPostTime = mSensitiveContentNotif.getSbn().getPostTime();
when(mNotifCollection.getEntry(mWorkProfileNotif.getKey())).thenReturn(mWorkProfileNotif);
-
+ when(mKeyguardInteractorLazy.get()).thenReturn(mKeyguardInteractor);
+ when(mKeyguardInteractor.isKeyguardDismissible())
+ .thenReturn(mock(StateFlow.class));
mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
mLockscreenUserManager.setUpWithPresenter(mPresenter);
@@ -504,11 +516,85 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedIfNotLocked() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ // Claim the device was last locked 1 day ago
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
+ // Device is not currently locked
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(false);
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedIfUnlockedSinceReceipt() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ // Device was locked after this notification arrived
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1));
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedIfNotLockedForLongEnough() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ // Device has been locked for 1 second before the notification came in, which is too short
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1));
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @DisableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedFlagDisabled() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ // Claim the device was last locked 1 day ago
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
public void testHasSensitiveContent_redacted() {
// Allow private notifications for this user
mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
mCurrentUser.id);
changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ // Claim the device was last unlocked 1 day ago
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
// Sensitive Content notifications are always redacted
assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT,
@@ -1066,7 +1152,9 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mock(DumpManager.class),
mock(LockPatternUtils.class),
mFakeFeatureFlags,
- mDeviceUnlockedInteractorLazy
+ mDeviceUnlockedInteractorLazy,
+ mKeyguardInteractorLazy,
+ null //CoroutineScope
);
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index a62d9d5ce62f..0061c4142e8b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -132,7 +132,7 @@ class CallChipViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chip)
repo.setOngoingCallState(
- inCallModel(startTimeMs = 1000, notificationIcon = mock<StatusBarIconView>())
+ inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull())
)
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -147,11 +147,12 @@ class CallChipViewModelTest : SysuiTestCase() {
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon))
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -165,6 +166,24 @@ class CallChipViewModelTest : SysuiTestCase() {
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ repo.setOngoingCallState(
+ inCallModel(
+ startTimeMs = 1000,
+ notificationIcon = createStatusBarIconViewOrNull(),
+ notificationKey = "notifKey",
+ )
+ )
+
+ assertThat((latest as OngoingActivityChipModel.Shown).icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -192,7 +211,7 @@ class CallChipViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chip)
repo.setOngoingCallState(
- inCallModel(startTimeMs = 0, notificationIcon = mock<StatusBarIconView>())
+ inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull())
)
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -207,11 +226,12 @@ class CallChipViewModelTest : SysuiTestCase() {
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun chip_zeroStartTime_notifIconFlagOn_iconIsNotifIcon() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon))
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -224,8 +244,27 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ repo.setOngoingCallState(
+ inCallModel(
+ startTimeMs = 0,
+ notificationIcon = createStatusBarIconViewOrNull(),
+ notificationKey = "notifKey",
+ )
+ )
+
+ assertThat((latest as OngoingActivityChipModel.Shown).icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+ }
+
+ @Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun chip_notifIconFlagOn_butNullNotifIcon_iconIsPhone() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -242,6 +281,24 @@ class CallChipViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ repo.setOngoingCallState(
+ inCallModel(
+ startTimeMs = 1000,
+ notificationIcon = null,
+ notificationKey = "notifKey",
+ )
+ )
+
+ assertThat((latest as OngoingActivityChipModel.Shown).icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+ }
+
+ @Test
fun chip_positiveStartTime_colorsAreThemed() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -330,4 +387,13 @@ class CallChipViewModelTest : SysuiTestCase() {
verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
}
+
+ companion object {
+ fun createStatusBarIconViewOrNull(): StatusBarIconView? =
+ if (StatusBarConnectedDisplays.isEnabled) {
+ null
+ } else {
+ mock<StatusBarIconView>()
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 0d033a4098ec..fe15eac46e2d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.notification.domain.interactor
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -148,7 +149,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
}
@Test
- fun notificationChip_missingStatusBarIconChipView_inConstructor_emitsNull() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_missingStatusBarIconChipView_cdFlagDisabled_inConstructor_emitsNull() =
kosmos.runTest {
val underTest =
factory.create(
@@ -167,6 +169,25 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_missingStatusBarIconChipView_cdFlagEnabled_inConstructor_emitsNotNull() =
+ kosmos.runTest {
+ val underTest =
+ factory.create(
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = null,
+ promotedContent = PROMOTED_CONTENT,
+ ),
+ 32L,
+ )
+
+ val latest by collectLastValue(underTest.notificationChip)
+
+ assertThat(latest).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun notificationChip_cdEnabled_missingStatusBarIconChipView_inConstructor_emitsNotNull() =
kosmos.runTest {
val underTest =
@@ -186,7 +207,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
}
@Test
- fun notificationChip_missingStatusBarIconChipView_inSet_emitsNull() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_cdFlagDisabled_missingStatusBarIconChipView_inSet_emitsNull() =
kosmos.runTest {
val startingNotif =
activeNotificationModel(
@@ -211,6 +233,31 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_cdFlagEnabled_missingStatusBarIconChipView_inSet_emitsNotNull() =
+ kosmos.runTest {
+ val startingNotif =
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = mock(),
+ promotedContent = PROMOTED_CONTENT,
+ )
+ val underTest = factory.create(startingNotif, 123L)
+ val latest by collectLastValue(underTest.notificationChip)
+ assertThat(latest).isNotNull()
+
+ underTest.setNotification(
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = null,
+ promotedContent = PROMOTED_CONTENT,
+ )
+ )
+
+ assertThat(latest).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun notificationChip_missingStatusBarIconChipView_inSet_cdEnabled_emitsNotNull() =
kosmos.runTest {
val startingNotif =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index f703d785ceac..ee4a52d35d68 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -30,6 +30,7 @@ import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -83,7 +84,8 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun notificationChips_notifMissingStatusBarChipIconView_empty() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() =
kosmos.runTest {
val latest by collectLastValue(underTest.notificationChips)
@@ -101,6 +103,25 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.notificationChips)
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = null,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ )
+ )
+ )
+
+ assertThat(latest).isNotEmpty()
+ }
+
+ @Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun notificationChips_onePromotedNotif_statusBarIconViewMatches() =
kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 17076b4d7505..e561e3ea27d7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -23,7 +23,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.kosmos.collectLastValue
@@ -31,6 +30,7 @@ import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
@@ -48,7 +48,6 @@ import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -84,8 +83,8 @@ class NotifChipsViewModelTest : SysuiTestCase() {
}
@Test
- @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
- fun chips_notifMissingStatusBarChipIconView_empty() =
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chips_notifMissingStatusBarChipIconView_cdFlagDisabled_empty() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
@@ -104,11 +103,31 @@ class NotifChipsViewModelTest : SysuiTestCase() {
@Test
@DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chips_notifMissingStatusBarChipIconView_cdFlagEnabled_notEmpty() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = null,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ )
+ )
+ )
+
+ assertThat(latest).isNotEmpty()
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
fun chips_onePromotedNotif_statusBarIconViewMatches() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
- val icon = mock<StatusBarIconView>()
+ val icon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -121,8 +140,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
assertThat(latest).hasSize(1)
val chip = latest!![0]
- assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat(chip.icon).isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(icon))
+ assertIsNotifChip(chip, icon, "notif")
}
@Test
@@ -168,7 +186,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -187,8 +205,8 @@ class NotifChipsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
- val firstIcon = mock<StatusBarIconView>()
- val secondIcon = mock<StatusBarIconView>()
+ val firstIcon = createStatusBarIconViewOrNull()
+ val secondIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -203,15 +221,15 @@ class NotifChipsViewModelTest : SysuiTestCase() {
),
activeNotificationModel(
key = "notif3",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = null,
),
)
)
assertThat(latest).hasSize(2)
- assertIsNotifChip(latest!![0], firstIcon)
- assertIsNotifChip(latest!![1], secondIcon)
+ assertIsNotifChip(latest!![0], firstIcon, "notif1")
+ assertIsNotifChip(latest!![1], secondIcon, "notif2")
}
@Test
@@ -269,7 +287,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -293,7 +311,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -323,7 +341,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -353,7 +371,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -382,7 +400,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -411,7 +429,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -439,7 +457,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -467,7 +485,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -499,7 +517,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -531,7 +549,7 @@ class NotifChipsViewModelTest : SysuiTestCase() {
listOf(
activeNotificationModel(
key = "clickTest",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent =
PromotedNotificationContentModel.Builder("clickTest").build(),
)
@@ -552,9 +570,21 @@ class NotifChipsViewModelTest : SysuiTestCase() {
}
companion object {
- fun assertIsNotifChip(latest: OngoingActivityChipModel?, expectedIcon: StatusBarIconView) {
- assertThat((latest as OngoingActivityChipModel.Shown).icon)
- .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon))
+ fun assertIsNotifChip(
+ latest: OngoingActivityChipModel?,
+ expectedIcon: StatusBarIconView?,
+ notificationKey: String,
+ ) {
+ val shown = latest as OngoingActivityChipModel.Shown
+ if (StatusBarConnectedDisplays.isEnabled) {
+ assertThat(shown.icon)
+ .isEqualTo(
+ OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notificationKey)
+ )
+ } else {
+ assertThat(latest.icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon!!))
+ }
}
fun assertIsNotifKey(latest: OngoingActivityChipModel?, expectedKey: String) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 4fb42e94adb2..42358cce59a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -41,6 +41,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -169,29 +170,35 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
@Test
fun primaryChip_screenRecordAndShareToAppAndCastToOtherHideAndCallShown_callShown() =
testScope.runTest {
+ val notificationKey = "call"
screenRecordState.value = ScreenRecordModel.DoingNothing
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = notificationKey)
+ )
val latest by collectLastValue(underTest.primaryChip)
- assertIsCallChip(latest)
+ assertIsCallChip(latest, notificationKey)
}
@Test
fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
testScope.runTest {
// Start with just the lowest priority chip shown
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
// And everything else hidden
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
val latest by collectLastValue(underTest.primaryChip)
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -218,7 +225,10 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.primaryChip)
@@ -235,7 +245,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
mediaProjectionState.value = MediaProjectionState.NotProjecting
// THEN the lower priority call is used
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
}
/** Regression test for b/347726238. */
@@ -364,13 +374,27 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
}
- fun assertIsCallChip(latest: OngoingActivityChipModel?) {
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ fun assertIsCallChip(latest: OngoingActivityChipModel?, notificationKey: String) {
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
+ if (StatusBarConnectedDisplays.isEnabled) {
+ assertNotificationIcon(latest, notificationKey)
+ return
+ }
val icon =
(((latest as OngoingActivityChipModel.Shown).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
.impl as Icon.Resource
assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
}
+
+ private fun assertNotificationIcon(
+ latest: OngoingActivityChipModel?,
+ notificationKey: String,
+ ) {
+ val shown = latest as OngoingActivityChipModel.Shown
+ val notificationIcon =
+ shown.icon as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+ assertThat(notificationIcon.notificationKey).isEqualTo(notificationKey)
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 0050ebee64d6..0f42f29e76ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -34,7 +34,7 @@ import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager
import com.android.systemui.res.R
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
-import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
@@ -186,13 +186,16 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chips_screenRecordShowAndCallShow_primaryIsScreenRecordSecondaryIsCall() =
testScope.runTest {
+ val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.chips)
assertIsScreenRecordChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
}
@Test
@@ -240,15 +243,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chips_shareToAppShowAndCallShow_primaryIsShareToAppSecondaryIsCall() =
testScope.runTest {
+ val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.chips)
assertIsShareToAppChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
}
@Test
@@ -258,25 +264,31 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.primaryChip)
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
}
@Test
fun chips_onlyCallShown_primaryIsCallSecondaryIsHidden() =
testScope.runTest {
+ val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.DoingNothing
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.chips)
- assertIsCallChip(latest!!.primary)
+ assertIsCallChip(latest!!.primary, callNotificationKey)
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@@ -285,7 +297,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- val icon = mock<StatusBarIconView>()
+ val icon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -296,7 +308,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
)
)
- assertIsNotifChip(latest!!.primary, icon)
+ assertIsNotifChip(latest!!.primary, icon, "notif")
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@@ -305,8 +317,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- val firstIcon = mock<StatusBarIconView>()
- val secondIcon = mock<StatusBarIconView>()
+ val firstIcon = createStatusBarIconViewOrNull()
+ val secondIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -324,8 +336,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
)
)
- assertIsNotifChip(latest!!.primary, firstIcon)
- assertIsNotifChip(latest!!.secondary, secondIcon)
+ assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif")
+ assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif")
}
@Test
@@ -333,9 +345,9 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- val firstIcon = mock<StatusBarIconView>()
- val secondIcon = mock<StatusBarIconView>()
- val thirdIcon = mock<StatusBarIconView>()
+ val firstIcon = createStatusBarIconViewOrNull()
+ val secondIcon = createStatusBarIconViewOrNull()
+ val thirdIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -359,8 +371,8 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
)
)
- assertIsNotifChip(latest!!.primary, firstIcon)
- assertIsNotifChip(latest!!.secondary, secondIcon)
+ assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif")
+ assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif")
}
@Test
@@ -368,8 +380,12 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- val firstIcon = mock<StatusBarIconView>()
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
+
+ val firstIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -380,43 +396,47 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
),
activeNotificationModel(
key = "secondNotif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent =
PromotedNotificationContentModel.Builder("secondNotif").build(),
),
)
)
- assertIsCallChip(latest!!.primary)
- assertIsNotifChip(latest!!.secondary, firstIcon)
+ assertIsCallChip(latest!!.primary, callNotificationKey)
+ assertIsNotifChip(latest!!.secondary, firstIcon, "firstNotif")
}
@Test
fun chips_screenRecordAndCallAndPromotedNotifs_notifsNotShown() =
testScope.runTest {
+ val callNotificationKey = "call"
val latest by collectLastValue(underTest.chips)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
screenRecordState.value = ScreenRecordModel.Recording
setNotifs(
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
)
assertIsScreenRecordChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
}
@Test
fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
testScope.runTest {
+ val callNotificationKey = "call"
// Start with just the lowest priority chip shown
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -433,13 +453,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.primaryChip)
- assertIsNotifChip(latest, notifIcon)
+ assertIsNotifChip(latest, notifIcon, "notif")
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
// THEN the higher priority call chip is used
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -462,12 +484,15 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun primaryChip_highestPriorityChipRemoved_showsNextPriorityChip() =
testScope.runTest {
+ val callNotificationKey = "call"
// WHEN all chips are active
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- val notifIcon = mock<StatusBarIconView>()
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
+ val notifIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -493,20 +518,21 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value = MediaProjectionState.NotProjecting
// THEN the lower priority call is used
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority call is removed
callRepo.setOngoingCallState(OngoingCallModel.NoCall)
// THEN the lower priority notif is used
- assertIsNotifChip(latest, notifIcon)
+ assertIsNotifChip(latest, notifIcon, "notif")
}
@Test
fun chips_movesChipsAroundAccordingToPriority() =
testScope.runTest {
+ val callNotificationKey = "call"
// Start with just the lowest priority chip shown
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -523,16 +549,18 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chips)
- assertIsNotifChip(latest!!.primary, notifIcon)
+ assertIsNotifChip(latest!!.primary, notifIcon, "notif")
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
// THEN the higher priority call chip is used as primary and notif is demoted to
// secondary
- assertIsCallChip(latest!!.primary)
- assertIsNotifChip(latest!!.secondary, notifIcon)
+ assertIsCallChip(latest!!.primary, callNotificationKey)
+ assertIsNotifChip(latest!!.secondary, notifIcon, "notif")
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -545,7 +573,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// THEN the higher priority media projection chip is used as primary and call is demoted
// to secondary (and notif is dropped altogether)
assertIsShareToAppChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
// WHEN the higher priority screen record chip is added
screenRecordState.value = ScreenRecordModel.Recording
@@ -559,13 +587,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// THEN media projection and notif remain
assertIsShareToAppChip(latest!!.primary)
- assertIsNotifChip(latest!!.secondary, notifIcon)
+ assertIsNotifChip(latest!!.secondary, notifIcon, "notif")
// WHEN media projection is dropped
mediaProjectionState.value = MediaProjectionState.NotProjecting
// THEN notif is promoted to primary
- assertIsNotifChip(latest!!.primary, notifIcon)
+ assertIsNotifChip(latest!!.primary, notifIcon, "notif")
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
index feda0c65bd7f..ab475c5edb76 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
@@ -32,10 +32,10 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.phone.BoundsPair
-import com.android.systemui.statusbar.phone.LetterboxAppearance
-import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.BoundsPair
+import com.android.systemui.statusbar.layout.LetterboxAppearance
+import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
index 4795a123617f..db51a586fcc1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
@@ -33,7 +33,7 @@ import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 5f3668af6e45..0a9601359397 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -27,8 +27,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.AnimatorTestRule
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
index 518b327036cb..f1affbc4db4f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.graphics.Color
import android.graphics.Rect
-import android.view.WindowInsetsController
+import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -42,7 +42,7 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
companion object {
private const val DEFAULT_APPEARANCE = 0
- private const val TEST_APPEARANCE = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+ private const val TEST_APPEARANCE = APPEARANCE_LIGHT_STATUS_BARS
private val TEST_APPEARANCE_REGION_BOUNDS = Rect(0, 0, 20, 100)
private val TEST_APPEARANCE_REGION =
AppearanceRegion(TEST_APPEARANCE, TEST_APPEARANCE_REGION_BOUNDS)
@@ -74,7 +74,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
expect
.that(letterboxAppearance.appearance)
@@ -90,7 +94,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
expect
.that(letterboxAppearance.appearance)
@@ -109,10 +117,10 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterBoxInnerBoundsCopy = Rect(letterBoxInnerBounds)
calculator.getLetterboxAppearance(
- TEST_APPEARANCE,
- TEST_APPEARANCE_REGIONS,
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
listOf(letterboxWithInnerBounds(letterBoxInnerBounds)),
- BoundsPair(statusBarStartSideBounds, statusBarEndSideBounds)
+ BoundsPair(statusBarStartSideBounds, statusBarEndSideBounds),
)
expect.that(statusBarStartSideBounds).isEqualTo(statusBarStartSideBoundsCopy)
@@ -129,11 +137,15 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
expect
- .that(letterboxAppearance.appearance)
- .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
+ .that(letterboxAppearance.appearance)
+ .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
}
@@ -145,7 +157,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -158,7 +174,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -171,7 +191,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -184,7 +208,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -198,7 +226,11 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, listOf(letterboxRegion), listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ listOf(letterboxRegion),
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
val letterboxAdaptedRegion = letterboxRegion.copy(bounds = letterbox.letterboxInnerBounds)
assertThat(letterboxAppearance.appearanceRegions.toList()).contains(letterboxAdaptedRegion)
@@ -212,12 +244,17 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
val letterbox =
letterboxWithBounds(
innerBounds = Rect(left = 25, top = 0, right = 75, bottom = 100),
- fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100))
+ fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100),
+ )
val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, listOf(letterboxRegion), listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ listOf(letterboxRegion),
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
val outerRegions =
listOf(
@@ -230,8 +267,7 @@ class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
Rect(left = 75, top = 0, right = 100, bottom = 100),
),
)
- assertThat(letterboxAppearance.appearanceRegions)
- .containsAtLeastElementsIn(outerRegions)
+ assertThat(letterboxAppearance.appearanceRegions).containsAtLeastElementsIn(outerRegions)
}
private fun letterboxWithBounds(innerBounds: Rect, fullBounds: Rect) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
index b9cfe21dcad3..04319f05f6f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.graphics.Rect
import android.testing.TestableLooper.RunWithLooper
@@ -23,7 +23,6 @@ import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider.BoundsChangeListener
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -47,7 +46,7 @@ class StatusBarBoundsProviderTest : SysuiTestCase() {
private val END_SIDE_BOUNDS = Rect(250, 300, 350, 400)
}
- @Mock private lateinit var boundsChangeListener: BoundsChangeListener
+ @Mock private lateinit var boundsChangeListener: StatusBarBoundsProvider.BoundsChangeListener
private lateinit var boundsProvider: StatusBarBoundsProvider
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
index 7a51b2ddb020..c9c961791e89 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.content.Context
import android.content.res.Configuration
@@ -32,6 +32,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.leak.RotationUtils
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index 92318b9820b8..ffd349d744a8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
@@ -53,6 +53,7 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.policy.BatteryController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
index 788c2cb2a485..7786689c84d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
@@ -25,6 +25,7 @@ import android.view.IWindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.layout.LetterboxBackgroundProvider
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
@@ -134,6 +135,7 @@ class LetterboxBackgroundProviderTest : SysuiTestCase() {
fun isLetterboxBackgroundMultiColored_defaultValue_returnsFalse() {
assertThat(provider.isLetterboxBackgroundMultiColored).isEqualTo(false)
}
+
@Test
fun isLetterboxBackgroundMultiColored_afterOnStart_executorNotDone_returnsDefaultValue() {
whenever(windowManager.isLetterboxBackgroundMultiColored).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index 9099334b360c..a65ccad6c2f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -51,6 +51,7 @@ import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.statusbar.data.model.StatusBarAppearance;
import com.android.systemui.statusbar.data.model.StatusBarMode;
import com.android.systemui.statusbar.data.repository.FakeStatusBarModePerDisplayRepository;
+import com.android.systemui.statusbar.layout.BoundsPair;
import com.android.systemui.statusbar.policy.BatteryController;
import kotlinx.coroutines.test.TestScope;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index d174484219ff..2e12336f6e93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -40,7 +40,6 @@ import static org.mockito.Mockito.when;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
-import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.TestableLooper;
@@ -610,7 +609,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_registration() {
/* verify that a predictive back callback is registered when the bouncer becomes visible */
mBouncerExpansionCallback.onVisibilityChanged(true);
@@ -625,7 +623,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_invocationHidesBouncer() {
mBouncerExpansionCallback.onVisibilityChanged(true);
/* capture the predictive back callback during registration */
@@ -643,7 +640,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
.thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
@@ -663,7 +659,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_forwardsBackDispatches() {
mBouncerExpansionCallback.onVisibilityChanged(true);
/* capture the predictive back callback during registration */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 0652a835cb7c..650fa7ce46de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -31,7 +31,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
-import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.ravenwood.RavenwoodRule;
@@ -41,7 +40,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.systemui.Dependency;
-import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.animation.back.BackAnimationSpec;
@@ -137,7 +135,6 @@ public class SystemUIDialogTest extends SysuiTestCase {
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS)
public void usePredictiveBackAnimFlag() {
final SystemUIDialog dialog = new SystemUIDialog(mContext);
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 3d6882c3fdaf..c6bae197ad76 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,9 +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.media.AudioManager
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION
@@ -34,6 +33,7 @@ import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.settingslib.notification.data.repository.updateNotificationPolicy
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
@@ -402,115 +402,124 @@ class ZenModeInteractorTest : SysuiTestCase() {
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
- fun activeModesBlockingEverything_hasModesWithFilterNone() =
+ fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
testScope.runTest {
- val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything)
+ val blockingMedia by
+ collectLastValue(
+ underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_MUSIC))
+ )
zenModeRepository.addModes(
listOf(
TestModeBuilder()
- .setName("Filter=None, Not active")
- .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setName("Blocks media, Not active")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
.setActive(false)
.build(),
TestModeBuilder()
- .setName("Filter=Priority, Active")
- .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setName("Allows media, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build())
.setActive(true)
.build(),
TestModeBuilder()
- .setName("Filter=None, Active")
- .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setName("Blocks media, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
.setActive(true)
.build(),
TestModeBuilder()
- .setName("Filter=None, Active Too")
- .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setName("Blocks media, Active Too")
+ .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
.setActive(true)
.build(),
)
)
runCurrent()
- assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active")
- assertThat(blockingEverything!!.modeNames)
- .containsExactly("Filter=None, Active", "Filter=None, Active Too")
+ 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 activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
+ fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
testScope.runTest {
- val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia)
+ val blockingAlarms by
+ collectLastValue(
+ underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_ALARM))
+ )
zenModeRepository.addModes(
listOf(
TestModeBuilder()
- .setName("Blocks media, Not active")
- .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setName("Blocks alarms, Not active")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
.setActive(false)
.build(),
TestModeBuilder()
- .setName("Allows media, Active")
- .setZenPolicy(ZenPolicy.Builder().allowMedia(true).build())
+ .setName("Allows alarms, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build())
.setActive(true)
.build(),
TestModeBuilder()
- .setName("Blocks media, Active")
- .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setName("Blocks alarms, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
.setActive(true)
.build(),
TestModeBuilder()
- .setName("Blocks media, Active Too")
- .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build())
+ .setName("Blocks alarms, Active Too")
+ .setZenPolicy(ZenPolicy.Builder().allowAlarms(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")
+ assertThat(blockingAlarms!!.mainMode!!.name).isEqualTo("Blocks alarms, Active")
+ assertThat(blockingAlarms!!.modeNames)
+ .containsExactly("Blocks alarms, Active", "Blocks alarms, Active Too")
.inOrder()
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
- fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
+ fun activeModesBlockingAlarms_hasModesWithPolicyBlockingSystem() =
testScope.runTest {
- val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms)
+ val blockingSystem by
+ collectLastValue(
+ underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_SYSTEM))
+ )
zenModeRepository.addModes(
listOf(
TestModeBuilder()
- .setName("Blocks alarms, Not active")
- .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setName("Blocks system, Not active")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
.setActive(false)
.build(),
TestModeBuilder()
- .setName("Allows alarms, Active")
- .setZenPolicy(ZenPolicy.Builder().allowAlarms(true).build())
+ .setName("Allows system, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(true).build())
.setActive(true)
.build(),
TestModeBuilder()
- .setName("Blocks alarms, Active")
- .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setName("Blocks system, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
.setActive(true)
.build(),
TestModeBuilder()
- .setName("Blocks alarms, Active Too")
- .setZenPolicy(ZenPolicy.Builder().allowAlarms(false).build())
+ .setName("Blocks system, Active Too")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(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")
+ assertThat(blockingSystem!!.mainMode!!.name).isEqualTo("Blocks system, Active")
+ assertThat(blockingSystem!!.modeNames)
+ .containsExactly("Blocks system, Active", "Blocks system, Active Too")
.inOrder()
}
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
index d3071f87f744..51cac6976362 100644
--- 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
@@ -23,66 +23,40 @@ 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.haptics.slider.sliderHapticsViewModelFactory
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
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(
+ private fun Kosmos.audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
+ return audioStreamSliderViewModelFactory.create(
AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
- testScope.backgroundScope,
- context,
- kosmos.audioVolumeInteractor,
- kosmos.zenModeInteractor,
- kosmos.uiEventLogger,
- kosmos.volumePanelLogger,
- kosmos.sliderHapticsViewModelFactory,
+ applicationCoroutineScope,
)
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
fun slider_media_hasDisabledByModesText() =
- testScope.runTest {
- val mediaSlider by collectLastValue(mediaStream.slider)
+ kosmos.runTest {
+ val mediaSlider by
+ collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_MUSIC).slider)
zenModeRepository.addMode(
TestModeBuilder()
@@ -112,8 +86,9 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
fun slider_alarms_hasDisabledByModesText() =
- testScope.runTest {
- val alarmsSlider by collectLastValue(alarmsStream.slider)
+ kosmos.runTest {
+ val alarmsSlider by
+ collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_ALARM).slider)
zenModeRepository.addMode(
TestModeBuilder()
@@ -141,9 +116,10 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
- fun slider_other_hasDisabledByModesText() =
- testScope.runTest {
- val otherSlider by collectLastValue(otherStream.slider)
+ fun slider_other_hasDisabledText() =
+ kosmos.runTest {
+ val otherSlider by
+ collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL).slider)
zenModeRepository.addMode(
TestModeBuilder()
@@ -154,20 +130,17 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
)
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)
+ kosmos.runTest {
+ val notificationSlider by
+ collectLastValue(
+ audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION).slider
+ )
runCurrent()
assertThat(notificationSlider!!.disabledMessage)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 7d220b505aa0..6e23a0783c9d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -21,11 +21,9 @@ import android.util.Log;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
-import android.window.TaskSnapshot;
import android.window.WindowAnimationState;
import com.android.internal.os.IResultReceiver;
-import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.wm.shell.recents.IRecentsAnimationController;
public class RecentsAnimationControllerCompat {
@@ -40,18 +38,6 @@ public class RecentsAnimationControllerCompat {
mAnimationController = animationController;
}
- public ThumbnailData screenshotTask(int taskId) {
- try {
- final TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
- if (snapshot != null) {
- return ThumbnailData.fromSnapshot(snapshot);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to screenshot task", e);
- }
- return new ThumbnailData();
- }
-
public void setInputConsumerEnabled(boolean enabled) {
try {
mAnimationController.setInputConsumerEnabled(enabled);
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 29785959de18..9596a540b63b 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -20,10 +20,12 @@ import android.content.Context
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent
import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
@@ -68,7 +70,7 @@ interface ContextualEducationRepository {
suspend fun updateGestureEduModel(
gestureType: GestureType,
- transform: (GestureEduModel) -> GestureEduModel
+ transform: (GestureEduModel) -> GestureEduModel,
)
suspend fun updateEduDeviceConnectionTime(
@@ -149,6 +151,8 @@ constructor(
String.format(DATASTORE_DIR, userId)
)
},
+ corruptionHandler =
+ ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
scope = newDsScope,
)
dataStoreScope = newDsScope
@@ -159,7 +163,7 @@ constructor(
private fun getGestureEduModel(
gestureType: GestureType,
- preferences: Preferences
+ preferences: Preferences,
): GestureEduModel {
return GestureEduModel(
signalCount = preferences[getSignalCountKey(gestureType)] ?: 0,
@@ -183,7 +187,7 @@ constructor(
override suspend fun updateGestureEduModel(
gestureType: GestureType,
- transform: (GestureEduModel) -> GestureEduModel
+ transform: (GestureEduModel) -> GestureEduModel,
) {
datastore.filterNotNull().first().edit { preferences ->
val currentModel = getGestureEduModel(gestureType, preferences)
@@ -193,17 +197,17 @@ constructor(
setInstant(
preferences,
updatedModel.lastShortcutTriggeredTime,
- getLastShortcutTriggeredTimeKey(gestureType)
+ getLastShortcutTriggeredTimeKey(gestureType),
)
setInstant(
preferences,
updatedModel.usageSessionStartTime,
- getUsageSessionStartTimeKey(gestureType)
+ getUsageSessionStartTimeKey(gestureType),
)
setInstant(
preferences,
updatedModel.lastEducationTime,
- getLastEducationTimeKey(gestureType)
+ getLastEducationTimeKey(gestureType),
)
}
}
@@ -220,12 +224,12 @@ constructor(
setInstant(
preferences,
updatedModel.keyboardFirstConnectionTime,
- getKeyboardFirstConnectionTimeKey()
+ getKeyboardFirstConnectionTimeKey(),
)
setInstant(
preferences,
updatedModel.touchpadFirstConnectionTime,
- getTouchpadFirstConnectionTimeKey()
+ getTouchpadFirstConnectionTimeKey(),
)
}
}
@@ -235,7 +239,7 @@ constructor(
keyboardFirstConnectionTime =
preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
touchpadFirstConnectionTime =
- preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }
+ preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
)
}
@@ -263,7 +267,7 @@ constructor(
private fun setInstant(
preferences: MutablePreferences,
instant: Instant?,
- key: Preferences.Key<Long>
+ key: Preferences.Key<Long>,
) {
if (instant != null) {
// Use epochSecond because an instant is defined as a signed long (64bit number) of
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
index 19a19d551613..c702ba9f401e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
@@ -25,6 +25,7 @@ import com.android.systemui.CoreStartable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.CommandQueue
@@ -41,6 +42,7 @@ constructor(
private val stateRepository: ShortcutHelperStateRepository,
private val activityStarter: ActivityStarter,
@Background private val backgroundScope: CoroutineScope,
+ private val customInputGesturesRepository: CustomInputGesturesRepository
) : CoreStartable {
override fun start() {
registerBroadcastReceiver(
@@ -55,6 +57,10 @@ constructor(
action = Intent.ACTION_CLOSE_SYSTEM_DIALOGS,
onReceive = { stateRepository.hide() },
)
+ registerBroadcastReceiver(
+ action = Intent.ACTION_USER_SWITCHED,
+ onReceive = { customInputGesturesRepository.refreshCustomInputGestures() },
+ )
commandQueue.addCallback(
object : CommandQueue.Callbacks {
override fun dismissKeyboardShortcutsMenu() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
index 36cd40052041..e5c638cbdfba 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
@@ -25,6 +25,7 @@ import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RES
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.InputSettings
import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.ERROR_OTHER
@@ -37,6 +38,7 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
+@SysUISingleton
class CustomInputGesturesRepository
@Inject
constructor(private val userTracker: UserTracker,
@@ -56,7 +58,7 @@ constructor(private val userTracker: UserTracker,
val customInputGestures =
_customInputGesture.onStart { refreshCustomInputGestures() }
- private fun refreshCustomInputGestures() {
+ fun refreshCustomInputGestures() {
setCustomInputGestures(inputGestures = retrieveCustomInputGestures())
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
index 5b28a3fa08de..a74384f61469 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -156,6 +156,13 @@ constructor(
setWmLockscreenState(lockscreenShowing = lockscreenShown)
}
+ /**
+ * Called when the keyguard going away remote animation is started, and we have a
+ * RemoteAnimationTarget to animate.
+ *
+ * This is triggered either by this class calling ATMS#keyguardGoingAway, or by WM directly,
+ * such as when an activity with FLAG_DISMISS_KEYGUARD is launched over a dismissible keyguard.
+ */
fun onKeyguardGoingAwayRemoteAnimationStart(
@WindowManager.TransitionOldType transit: Int,
apps: Array<RemoteAnimationTarget>,
@@ -163,19 +170,32 @@ constructor(
nonApps: Array<RemoteAnimationTarget>,
finishedCallback: IRemoteAnimationFinishedCallback,
) {
- // Make sure this is true - we set it true when requesting keyguardGoingAway, but there are
- // cases where WM starts this transition on its own.
- isKeyguardGoingAway = true
+ // If we weren't expecting the keyguard to be going away, WM triggered this transition.
+ if (!isKeyguardGoingAway) {
+ // Since WM triggered this, we're likely not transitioning to GONE yet. See if we can
+ // start that transition.
+ val startedDismiss =
+ keyguardDismissTransitionInteractor.startDismissKeyguardTransition(
+ reason = "Going away remote animation started"
+ )
- // Ensure that we've started a dismiss keyguard transition. WindowManager can start the
- // going away animation on its own, if an activity launches and then requests dismissing the
- // keyguard. In this case, this is the first and only signal we'll receive to start
- // a transition to GONE. This transition needs to start even if we're not provided an app
- // animation target - it's possible the app is destroyed on creation, etc. but we'll still
- // be unlocking.
- keyguardDismissTransitionInteractor.startDismissKeyguardTransition(
- reason = "Going away remote animation started"
- )
+ if (!startedDismiss) {
+ // If the transition wasn't started, we're already GONE. This can happen with timing
+ // issues, where the remote animation took a long time to start, and something else
+ // caused us to unlock in the meantime. Since we're already GONE, simply end the
+ // remote animatiom immediately.
+ Log.d(
+ TAG,
+ "onKeyguardGoingAwayRemoteAnimationStart: " +
+ "Dismiss transition was not started; we're already GONE. " +
+ "Ending remote animation.",
+ )
+ finishedCallback.onAnimationFinished()
+ return
+ }
+
+ isKeyguardGoingAway = true
+ }
if (apps.isNotEmpty()) {
goingAwayRemoteAnimationFinishedCallback = finishedCallback
@@ -278,6 +298,6 @@ constructor(
}
companion object {
- private val TAG = WindowManagerLockscreenVisibilityManager::class.java.simpleName
+ private val TAG = "WindowManagerLsVis"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
index 4793d95b121c..089e5dc42df3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
@@ -48,9 +48,12 @@ constructor(
*
* This is called exclusively by sources that can authoritatively say we should be unlocked,
* including KeyguardSecurityContainerController and WindowManager.
+ *
+ * Returns [false] if the transition was not started, because we're already GONE or we don't
+ * know how to dismiss keyguard from the current state.
*/
- fun startDismissKeyguardTransition(reason: String = "") {
- if (SceneContainerFlag.isEnabled) return
+ fun startDismissKeyguardTransition(reason: String = ""): Boolean {
+ if (SceneContainerFlag.isEnabled) return false
Log.d(TAG, "#startDismissKeyguardTransition(reason=$reason)")
val startedState =
if (transitionRaceCondition()) {
@@ -65,13 +68,20 @@ constructor(
AOD -> fromAodTransitionInteractor.dismissAod()
DOZING -> fromDozingTransitionInteractor.dismissFromDozing()
KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.dismissFromOccluded()
- KeyguardState.GONE ->
+ KeyguardState.GONE -> {
Log.i(
TAG,
"Already transitioning to GONE; ignoring startDismissKeyguardTransition.",
)
- else -> Log.e(TAG, "We don't know how to dismiss keyguard from state $startedState.")
+ return false
+ }
+ else -> {
+ Log.e(TAG, "We don't know how to dismiss keyguard from state $startedState.")
+ return false
+ }
}
+
+ return true
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index 2497defba5a2..d981eeb0989b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.util.MathUtils
+import com.android.systemui.Flags.lightRevealMigration
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -32,9 +33,7 @@ import kotlinx.coroutines.flow.Flow
@SysUISingleton
class AodToOccludedTransitionViewModel
@Inject
-constructor(
- animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow.setup(
duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
@@ -52,10 +51,20 @@ constructor(
var currentAlpha = 0f
return transitionAnimation.sharedFlow(
duration = 250.milliseconds,
- startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
- onStart = { currentAlpha = viewState.alpha() },
+ startTime =
+ if (lightRevealMigration()) {
+ 100.milliseconds // Wait for the light reveal to "hit" the LS elements.
+ } else {
+ 0.milliseconds
+ },
+ onStart = {
+ if (lightRevealMigration()) {
+ currentAlpha = viewState.alpha()
+ } else {
+ currentAlpha = 0f
+ }
+ },
onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
- onCancel = { 0f },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index af37eea102ca..1a2238cfbc9e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -234,8 +234,6 @@ public abstract class MediaOutputBaseAdapter extends
(ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
: mController.getItemMarginEndDefault();
- mTitleIcon.setBackgroundTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 1dbb31708fe1..15afd22a27d8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -37,9 +37,6 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.AudioManager;
@@ -535,17 +532,9 @@ public class MediaSwitchingController
// Use default Bluetooth device icon to handle getIcon() is null case.
drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
}
- if (!(drawable instanceof BitmapDrawable)) {
- setColorFilter(drawable, isActiveItem(device));
- }
return BluetoothUtils.createIconWithDrawable(drawable);
}
- void setColorFilter(Drawable drawable, boolean isActive) {
- drawable.setColorFilter(new PorterDuffColorFilter(mColorItemContent,
- PorterDuff.Mode.SRC_IN));
- }
-
boolean isActiveItem(MediaDevice device) {
boolean isConnected = mLocalMediaManager.getCurrentConnectedDevice().getId().equals(
device.getId());
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index e9b7534f55e6..3f14b55e46a1 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -44,6 +44,7 @@ import android.hardware.display.DisplayManager;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.InputMethodService.BackDispositionMode;
import android.inputmethodservice.InputMethodService.ImeWindowVisibility;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Log;
@@ -61,6 +62,7 @@ import com.android.internal.statusbar.LetterboxDetails;
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
@@ -182,15 +184,18 @@ public class TaskbarDelegate implements CommandQueue.Callbacks,
private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private final StatusBarStateController mStatusBarStateController;
private DisplayTracker mDisplayTracker;
+ private final Handler mBgHandler;
@Inject
public TaskbarDelegate(Context context,
LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
- StatusBarStateController statusBarStateController) {
+ StatusBarStateController statusBarStateController,
+ @Background Handler bgHandler) {
mLightBarTransitionsControllerFactory = lightBarTransitionsControllerFactory;
mContext = context;
+ mBgHandler = bgHandler;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mPipListener = (bounds) -> {
mEdgeBackGestureHandler.setPipStashExclusionBounds(bounds);
@@ -245,7 +250,9 @@ public class TaskbarDelegate implements CommandQueue.Callbacks,
new LightBarTransitionsController.DarkIntensityApplier() {
@Override
public void applyDarkIntensity(float darkIntensity) {
- mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
+ mBgHandler.post(() -> {
+ mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
+ });
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index d401b6ecbfd8..a98b8e5ffdda 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -784,7 +784,11 @@ constructor(
private fun updateLongPressEffect(handlesLongClick: Boolean) {
// The long press effect in the tile can't be updated if it is still running
- if (longPressEffect?.state != QSLongPressEffect.State.IDLE) return
+ if (
+ longPressEffect?.state != QSLongPressEffect.State.IDLE &&
+ longPressEffect?.state != QSLongPressEffect.State.CLICKED
+ )
+ return
longPressEffect.qsTile?.state?.handlesLongClick = handlesLongClick
if (handlesLongClick && longPressEffect.initializeEffect(longPressEffectDuration)) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index dc0c4dc0b01c..2bcd1d1a1e51 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -24,7 +24,6 @@ import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
-import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag
/** Helper for reading or using the scene container flag state. */
object SceneContainerFlag {
@@ -36,8 +35,7 @@ object SceneContainerFlag {
get() =
sceneContainer() && // mainAconfigFlag
KeyguardWmStateRefactor.isEnabled &&
- NotificationThrottleHun.isEnabled &&
- PredictiveBackSysUiFlag.isEnabled
+ NotificationThrottleHun.isEnabled
// NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
@@ -49,7 +47,6 @@ object SceneContainerFlag {
sequenceOf(
KeyguardWmStateRefactor.token,
NotificationThrottleHun.token,
- PredictiveBackSysUiFlag.token,
// NOTE: Changes should also be made in isEnabled and @EnableSceneContainer
)
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
index e5ff2529e335..f295c0ccb3de 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -19,6 +19,7 @@ import android.content.Context
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.os.UserHandle
+import android.view.View
import androidx.annotation.StyleRes
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
@@ -119,6 +120,12 @@ class ScreenRecordPermissionDialogDelegate(
super<BaseMediaProjectionPermissionDialogDelegate>.onCreate(dialog, savedInstanceState)
setDialogTitle(R.string.screenrecord_permission_dialog_title)
dialog.setTitle(R.string.screenrecord_title)
+ setStartButtonOnClickListener { v: View? ->
+ val screenRecordViewBinder: ScreenRecordPermissionViewBinder? =
+ viewBinder as ScreenRecordPermissionViewBinder?
+ screenRecordViewBinder?.startButtonOnClicked()
+ dialog.dismiss()
+ }
setCancelButtonOnClickListener { dialog.dismiss() }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
index 9f7e1ade964a..691bdd4a1b27 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
@@ -79,37 +79,38 @@ class ScreenRecordPermissionViewBinder(
override fun bind() {
super.bind()
initRecordOptionsView()
- setStartButtonOnClickListener { _: View? ->
- onStartRecordingClicked?.run()
- if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
- requestScreenCapture(
- captureTarget = null,
- displayId = selectedScreenShareOption.displayId,
- )
- }
- if (selectedScreenShareOption.mode == SINGLE_APP) {
- val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ setStartButtonOnClickListener { startButtonOnClicked() }
+ }
- // We can't start activity for result here so we use result receiver to get
- // the selected target to capture
- intent.putExtra(
- MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
- CaptureTargetResultReceiver(),
- )
+ fun startButtonOnClicked() {
+ onStartRecordingClicked?.run()
+ if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
+ requestScreenCapture(
+ captureTarget = null,
+ displayId = selectedScreenShareOption.displayId,
+ )
+ }
+ if (selectedScreenShareOption.mode == SINGLE_APP) {
+ val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- intent.putExtra(
- MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
- hostUserHandle,
- )
- intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
- intent.putExtra(
- MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
- MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
- )
- activityStarter.startActivity(intent, /* dismissShade= */ true)
- }
- dialog.dismiss()
+ // We can't start activity for result here so we use result receiver to get
+ // the selected target to capture
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
+ CaptureTargetResultReceiver(),
+ )
+
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+ hostUserHandle,
+ )
+ intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
+ MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
+ )
+ activityStarter.startActivity(intent, /* dismissShade= */ true)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index e1631ccdcb06..bbb13d5c1dfe 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -61,9 +61,18 @@ interface UserTracker : UserContentResolverProvider, UserContextProvider {
/** Callback for notifying of changes. */
@WeaklyReferencedCallback
interface Callback {
- /** Notifies that the current user will be changed. */
+ /**
+ * Same as {@link onBeforeUserSwitching(Int, Runnable)} but the callback will be called
+ * automatically after the completion of this method.
+ */
fun onBeforeUserSwitching(newUser: Int) {}
+ /** Notifies that the current user will be changed. */
+ fun onBeforeUserSwitching(newUser: Int, resultCallback: Runnable) {
+ onBeforeUserSwitching(newUser)
+ resultCallback.run()
+ }
+
/**
* Same as {@link onUserChanging(Int, Context, Runnable)} but the callback will be called
* automatically after the completion of this method.
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index b7a3aedc565e..42d83637ec1a 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -196,8 +196,9 @@ internal constructor(
private fun registerUserSwitchObserver() {
iActivityManager.registerUserSwitchObserver(
object : UserSwitchObserver() {
- override fun onBeforeUserSwitching(newUserId: Int) {
+ override fun onBeforeUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
handleBeforeUserSwitching(newUserId)
+ reply?.sendResult(null)
}
override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
@@ -236,8 +237,7 @@ internal constructor(
setUserIdInternal(newUserId)
notifySubscribers { callback, resultCallback ->
- callback.onBeforeUserSwitching(newUserId)
- resultCallback.run()
+ callback.onBeforeUserSwitching(newUserId, resultCallback)
}
.await()
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index ef62d2da9589..a2edd3ab837d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -15,11 +15,18 @@
*/
package com.android.systemui.shade.data.repository
+import android.annotation.SuppressLint
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
/** Data for the shade, mostly related to expansion of the shade and quick settings. */
interface ShadeRepository {
@@ -36,7 +43,7 @@ interface ShadeRepository {
* Information about the currently running fling animation, or null if no fling animation is
* running.
*/
- val currentFling: StateFlow<FlingInfo?>
+ val currentFling: SharedFlow<FlingInfo?>
/**
* The amount the lockscreen shade has dragged down by the user, [0-1]. 0 means fully collapsed,
@@ -180,7 +187,8 @@ interface ShadeRepository {
/** Business logic for shade interactions */
@SysUISingleton
-class ShadeRepositoryImpl @Inject constructor() : ShadeRepository {
+class ShadeRepositoryImpl @Inject constructor(@Background val backgroundScope: CoroutineScope) :
+ ShadeRepository {
private val _qsExpansion = MutableStateFlow(0f)
@Deprecated("Use ShadeInteractor.qsExpansion instead")
override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow()
@@ -193,8 +201,13 @@ class ShadeRepositoryImpl @Inject constructor() : ShadeRepository {
override val udfpsTransitionToFullShadeProgress: StateFlow<Float> =
_udfpsTransitionToFullShadeProgress.asStateFlow()
- private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
- override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+ /**
+ * Must be a SharedFlow, since the fling is by definition an event and dropping it has extreme
+ * consequences in some cases (for example, keyguard uses this to decide when to unlock).
+ */
+ @SuppressLint("SharedFlowCreation")
+ override val currentFling: MutableSharedFlow<FlingInfo?> =
+ MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val _legacyShadeExpansion = MutableStateFlow(0f)
@Deprecated("Use ShadeInteractor.shadeExpansion instead")
@@ -294,7 +307,7 @@ class ShadeRepositoryImpl @Inject constructor() : ShadeRepository {
}
override fun setCurrentFling(info: FlingInfo?) {
- _currentFling.value = info
+ backgroundScope.launch { currentFling.emit(info) }
}
@Deprecated("Should only be called by NPVC and tests")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 239257d357f2..2bcd3fcfed17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -61,11 +61,13 @@ import com.android.systemui.Dumpable;
import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.recents.OverviewProxyService;
@@ -78,6 +80,7 @@ import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedac
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ListenerSet;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
import com.android.systemui.util.settings.SecureSettings;
import dagger.Lazy;
@@ -88,9 +91,13 @@ import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Handles keeping track of the current user, profiles, and various things related to hiding
* contents, redacting notifications, and the lockscreen.
@@ -111,6 +118,9 @@ public class NotificationLockscreenUserManagerImpl implements
private static final Uri SHOW_PRIVATE_LOCKSCREEN =
Settings.Secure.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS =
+ TimeUnit.MINUTES.toMillis(10);
+
private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy;
private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy;
private final DevicePolicyManager mDevicePolicyManager;
@@ -284,7 +294,12 @@ public class NotificationLockscreenUserManagerImpl implements
protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>();
+ // The last lock time. Uses currentTimeMillis
+ @VisibleForTesting
+ protected final AtomicLong mLastLockTime = new AtomicLong(-1);
+
protected int mCurrentUserId = 0;
+
protected NotificationPresenter mPresenter;
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
@@ -311,7 +326,10 @@ public class NotificationLockscreenUserManagerImpl implements
DumpManager dumpManager,
LockPatternUtils lockPatternUtils,
FeatureFlagsClassic featureFlags,
- Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) {
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
+ Lazy<KeyguardInteractor> keyguardInteractor,
+ @Application CoroutineScope coroutineScope
+ ) {
mContext = context;
mMainExecutor = mainExecutor;
mBackgroundExecutor = backgroundExecutor;
@@ -341,6 +359,18 @@ public class NotificationLockscreenUserManagerImpl implements
if (keyguardPrivateNotifications()) {
init();
}
+
+ // To avoid dependency injection cycle, finish constructing this object before using the
+ // KeyguardInteractor. The CoroutineScope will only be null in tests.
+ if (LockscreenOtpRedaction.isEnabled() && coroutineScope != null) {
+ mMainExecutor.execute(() -> JavaAdapterKt.collectFlow(coroutineScope,
+ keyguardInteractor.get().isKeyguardDismissible(),
+ unlocked -> {
+ if (!unlocked) {
+ mLastLockTime.set(System.currentTimeMillis());
+ }
+ }));
+ }
}
public void setUpWithPresenter(NotificationPresenter presenter) {
@@ -443,7 +473,7 @@ public class NotificationLockscreenUserManagerImpl implements
mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late
updateCurrentProfilesCache();
- // Set up
+ // Set up
mBackgroundExecutor.execute(() -> {
@SuppressLint("MissingPermission") List<UserInfo> users = mUserManager.getUsers();
for (int i = users.size() - 1; i >= 0; i--) {
@@ -667,8 +697,6 @@ public class NotificationLockscreenUserManagerImpl implements
!userAllowsPrivateNotificationsInPublic(mCurrentUserId);
boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId);
boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId);
- boolean isNotifSensitive = LockscreenOtpRedaction.isEnabled()
- && ent.getRanking() != null && ent.getRanking().hasSensitiveContent();
// redact notifications if the current user is redacting notifications or the notification
// contains sensitive content. However if the notification is associated with a managed
@@ -689,12 +717,45 @@ public class NotificationLockscreenUserManagerImpl implements
if (keyguardPrivateNotifications() && !mKeyguardAllowingNotifications) {
return REDACTION_TYPE_PUBLIC;
}
- if (isNotifSensitive) {
+
+ if (shouldShowSensitiveContentRedactedView(ent)) {
return REDACTION_TYPE_SENSITIVE_CONTENT;
}
return REDACTION_TYPE_NONE;
}
+ /*
+ * We show the sensitive content redaction view if
+ * 1. The feature is enabled
+ * 2. The device is locked
+ * 3. The notification has the `hasSensitiveContent` ranking variable set to true
+ * 4. The device has been locked for at least LOCK_TIME_FOR_SENSITIVE_REDACTION_MS
+ * 5. The notification arrived since the last lock time
+ */
+ private boolean shouldShowSensitiveContentRedactedView(NotificationEntry ent) {
+ if (!LockscreenOtpRedaction.isEnabled()) {
+ return false;
+ }
+
+ if (!mKeyguardManager.isDeviceLocked()) {
+ return false;
+ }
+
+ if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) {
+ return false;
+ }
+
+ long lastLockedTime = mLastLockTime.get();
+ if (ent.getSbn().getPostTime() < lastLockedTime) {
+ return false;
+ }
+
+ if ((System.currentTimeMillis() - lastLockedTime) < LOCK_TIME_FOR_SENSITIVE_REDACTION_MS) {
+ return false;
+ }
+ return true;
+ }
+
private boolean packageHasVisibilityOverride(String key) {
if (mCommonNotifCollectionLazy.get() == null) {
Log.wtf(TAG, "mEntryManager was null!", new Throwable());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 059e69a9275b..69ef09d8bf5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -242,8 +242,9 @@ object OngoingActivityChipBinder {
chipTimeView: ChipChronometer,
chipShortTimeDeltaView: DateTimeView,
) {
- if (chipModel.icon != null) {
- if (chipModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView) {
+ val icon = chipModel.icon
+ if (icon != null) {
+ if (iconRequiresEmbeddedPadding(icon)) {
// If the icon is a custom [StatusBarIconView], then it should've come from
// `Notification.smallIcon`, which is required to embed its own paddings. We need to
// adjust the other paddings to make everything look good :)
@@ -265,6 +266,10 @@ object OngoingActivityChipBinder {
}
}
+ private fun iconRequiresEmbeddedPadding(icon: OngoingActivityChipModel.ChipIcon) =
+ icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
+ icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+
private fun View.setTextPaddingForEmbeddedPaddingIcon() {
val newPaddingEnd =
context.resources.getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
new file mode 100644
index 000000000000..3d768d2d3e1e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.chips.ui.compose
+
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.rememberTextMeasurer
+import com.android.systemui.res.R
+
+/**
+ * Renders text within a status bar chip. The text is only displayed if more than 50% of its width
+ * can fit inside the bounds of the chip. If there is any overflow,
+ * [R.dimen.ongoing_activity_chip_text_fading_edge_length] is used to fade out the edge of the text.
+ */
+@Composable
+fun ChipText(
+ text: String,
+ backgroundColor: Color,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ style: TextStyle = LocalTextStyle.current,
+ minimumVisibleRatio: Float = 0.5f,
+) {
+ val density = LocalDensity.current
+ val textMeasurer = rememberTextMeasurer()
+
+ val textFadeLength =
+ dimensionResource(id = R.dimen.ongoing_activity_chip_text_fading_edge_length)
+ val maxTextWidthDp = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
+ val maxTextWidthPx = with(density) { maxTextWidthDp.toPx() }
+
+ val textLayoutResult = remember(text, style) { textMeasurer.measure(text, style) }
+ val willOverflowWidth = textLayoutResult.size.width > maxTextWidthPx
+
+ if (isSufficientlyVisible(maxTextWidthPx, minimumVisibleRatio, textLayoutResult)) {
+ Text(
+ text = text,
+ style = style,
+ softWrap = false,
+ color = color,
+ modifier =
+ modifier
+ .sizeIn(maxWidth = maxTextWidthDp)
+ .then(
+ if (willOverflowWidth) {
+ Modifier.overflowFadeOut(
+ with(density) { textFadeLength.roundToPx() },
+ backgroundColor,
+ )
+ } else {
+ Modifier
+ }
+ ),
+ )
+ }
+}
+
+private fun Modifier.overflowFadeOut(fadeLength: Int, color: Color): Modifier = drawWithContent {
+ drawContent()
+
+ val brush =
+ Brush.horizontalGradient(
+ colors = listOf(Color.Transparent, color),
+ startX = size.width - fadeLength,
+ endX = size.width,
+ )
+ drawRect(
+ brush = brush,
+ topLeft = Offset(size.width - fadeLength, 0f),
+ size = Size(fadeLength.toFloat(), size.height),
+ )
+}
+
+/**
+ * Returns `true` if at least [minimumVisibleRatio] of the text width fits within the given
+ * [maxAvailableWidthPx].
+ */
+@Composable
+private fun isSufficientlyVisible(
+ maxAvailableWidthPx: Float,
+ minimumVisibleRatio: Float,
+ textLayoutResult: TextLayoutResult,
+): Boolean {
+ val widthPx = textLayoutResult.size.width
+
+ return (maxAvailableWidthPx / widthPx) > minimumVisibleRatio
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 254b792f8152..d327fc23fd06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.dagger;
-import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
-
import android.content.Context;
import android.os.Handler;
import android.os.RemoteException;
@@ -28,7 +26,6 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.CoreStartable;
import com.android.systemui.animation.ActivityTransitionAnimator;
-import com.android.systemui.animation.AnimationFeatureFlags;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
@@ -226,8 +223,7 @@ public interface CentralSurfacesDependenciesModule {
IDreamManager dreamManager,
KeyguardStateController keyguardStateController,
Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
- InteractionJankMonitor interactionJankMonitor,
- AnimationFeatureFlags animationFeatureFlags) {
+ InteractionJankMonitor interactionJankMonitor) {
DialogTransitionAnimator.Callback callback = new DialogTransitionAnimator.Callback() {
@Override
public boolean isDreaming() {
@@ -249,19 +245,6 @@ public interface CentralSurfacesDependenciesModule {
return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint();
}
};
- return new DialogTransitionAnimator(
- mainExecutor, callback, interactionJankMonitor, animationFeatureFlags);
- }
-
- /** */
- @Provides
- @SysUISingleton
- static AnimationFeatureFlags provideAnimationFeatureFlags() {
- return new AnimationFeatureFlags() {
- @Override
- public boolean isPredictiveBackQsDialogAnim() {
- return predictiveBackAnimateDialogs();
- }
- };
+ return new DialogTransitionAnimator(mainExecutor, callback, interactionJankMonitor);
}
}
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 46c84fbc19aa..eff959d0f83b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -27,12 +27,12 @@ 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.data.repository.LightBarControllerStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
import com.android.systemui.statusbar.phone.AutoHideController
import com.android.systemui.statusbar.phone.AutoHideControllerImpl
import com.android.systemui.statusbar.phone.LightBarController
import com.android.systemui.statusbar.phone.LightBarControllerImpl
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
@@ -91,9 +91,7 @@ interface StatusBarModule {
@SysUISingleton
@IntoMap
@ClassKey(OngoingCallController::class)
- fun ongoingCallController(
- controller: OngoingCallController
- ): CoreStartable =
+ fun ongoingCallController(controller: OngoingCallController): CoreStartable =
if (StatusBarChipsModernization.isEnabled) {
CoreStartable.NOP
} else {
@@ -104,9 +102,7 @@ interface StatusBarModule {
@SysUISingleton
@IntoMap
@ClassKey(OngoingCallInteractor::class)
- fun ongoingCallInteractor(
- interactor: OngoingCallInteractor
- ): CoreStartable =
+ fun ongoingCallInteractor(interactor: OngoingCallInteractor): CoreStartable =
if (StatusBarChipsModernization.isEnabled) {
interactor
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
index 0cd31d06f71a..b7b91fa662b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.data.model
import com.android.internal.view.AppearanceRegion
-import com.android.systemui.statusbar.phone.BoundsPair
+import com.android.systemui.statusbar.layout.BoundsPair
/** Keeps track of various parameters coordinating the appearance of the status bar. */
data class StatusBarAppearance(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
index 554c46f6c219..5ea12110b00c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
@@ -28,8 +28,8 @@ import com.android.systemui.display.data.repository.PerDisplayStore
import com.android.systemui.display.data.repository.PerDisplayStoreImpl
import com.android.systemui.display.data.repository.SingleDisplayStore
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
import dagger.Lazy
import dagger.Module
import dagger.Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
index 22c37df7db7e..7fa9f0e9dcc2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
@@ -33,9 +33,9 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
import com.android.systemui.statusbar.data.model.StatusBarAppearance
import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.phone.BoundsPair
-import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.BoundsPair
+import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
@@ -209,9 +209,7 @@ constructor(
override val ongoingProcessRequiresStatusBarVisible =
_ongoingProcessRequiresStatusBarVisible.asStateFlow()
- override fun setOngoingProcessRequiresStatusBarVisible(
- requiredVisible: Boolean
- ) {
+ override fun setOngoingProcessRequiresStatusBarVisible(requiredVisible: Boolean) {
_ongoingProcessRequiresStatusBarVisible.value = requiredVisible
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index f7bc23c6eb17..63410d7465b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -41,8 +41,8 @@ import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.leak.RotationUtils
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 1038ad4124ba..70632b33f532 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -36,8 +36,8 @@ import com.android.systemui.dagger.qualifiers.Default
import com.android.systemui.res.R
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
index 231a8c65a246..1469fe78926e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.annotation.ColorInt
import android.content.Context
@@ -39,7 +39,7 @@ data class LetterboxAppearance(
) {
override fun toString(): String {
val appearanceString =
- ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
+ ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
return "LetterboxAppearance{$appearanceString, $appearanceRegions}"
}
}
@@ -57,14 +57,16 @@ constructor(
private val letterboxBackgroundProvider: LetterboxBackgroundProvider,
) : Dumpable {
- private val darkAppearanceIconColor = context.getColor(
- // For a dark background status bar, use a *light* icon color.
- com.android.settingslib.R.color.light_mode_icon_color_single_tone
- )
- private val lightAppearanceIconColor = context.getColor(
- // For a light background status bar, use a *dark* icon color.
- com.android.settingslib.R.color.dark_mode_icon_color_single_tone
- )
+ private val darkAppearanceIconColor =
+ context.getColor(
+ // For a dark background status bar, use a *light* icon color.
+ com.android.settingslib.R.color.light_mode_icon_color_single_tone
+ )
+ private val lightAppearanceIconColor =
+ context.getColor(
+ // For a light background status bar, use a *dark* icon color.
+ com.android.settingslib.R.color.dark_mode_icon_color_single_tone
+ )
init {
dumpManager.registerCriticalDumpable(this)
@@ -85,7 +87,11 @@ constructor(
lastAppearanceRegions = originalAppearanceRegions
lastLetterboxes = letterboxes
return getLetterboxAppearanceInternal(
- letterboxes, originalAppearance, originalAppearanceRegions, statusBarBounds)
+ letterboxes,
+ originalAppearance,
+ originalAppearanceRegions,
+ statusBarBounds,
+ )
.also { lastLetterboxAppearance = it }
}
@@ -118,7 +124,7 @@ constructor(
private fun getAppearanceRegions(
originalAppearanceRegions: List<AppearanceRegion>,
- letterboxes: List<LetterboxDetails>
+ letterboxes: List<LetterboxDetails>,
): List<AppearanceRegion> {
return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
getAllOuterAppearanceRegions(letterboxes)
@@ -126,7 +132,7 @@ constructor(
private fun sanitizeAppearanceRegions(
originalAppearanceRegions: List<AppearanceRegion>,
- letterboxes: List<LetterboxDetails>
+ letterboxes: List<LetterboxDetails>,
): List<AppearanceRegion> =
originalAppearanceRegions.map { appearanceRegion ->
val matchingLetterbox =
@@ -138,17 +144,20 @@ constructor(
// full bounds of its window.
// Here we want the bounds to be only for the inner bounds of the letterboxed app.
AppearanceRegion(
- appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
+ appearanceRegion.appearance,
+ matchingLetterbox.letterboxInnerBounds,
+ )
}
}
private fun originalAppearanceWithScrim(
@Appearance originalAppearance: Int,
- originalAppearanceRegions: List<AppearanceRegion>
+ originalAppearanceRegions: List<AppearanceRegion>,
): LetterboxAppearance {
return LetterboxAppearance(
originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
- originalAppearanceRegions)
+ originalAppearanceRegions,
+ )
}
@Appearance
@@ -215,7 +224,9 @@ constructor(
lastAppearanceRegion: $lastAppearanceRegions,
lastLetterboxes: $lastLetterboxes,
lastLetterboxAppearance: $lastLetterboxAppearance
- """.trimIndent())
+ """
+ .trimIndent()
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
index 34c7059ec991..3d8ced1b865f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.annotation.ColorInt
import android.app.WallpaperManager
@@ -49,9 +49,7 @@ constructor(
private set
private val wallpaperColorsListener =
- WallpaperManager.OnColorsChangedListener { _, _ ->
- fetchBackgroundColorInfo()
- }
+ WallpaperManager.OnColorsChangedListener { _, _ -> fetchBackgroundColorInfo() }
override fun start() {
fetchBackgroundColorInfo()
@@ -75,6 +73,8 @@ constructor(
"""
letterboxBackgroundColor: ${Color.valueOf(letterboxBackgroundColor)}
isLetterboxBackgroundMultiColored: $isLetterboxBackgroundMultiColored
- """.trimIndent())
+ """
+ .trimIndent()
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
index 3ac0bac95d68..ac5b037807a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.graphics.Rect
import android.view.View
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
index 41db5f450df8..f7a9094e0337 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.annotation.Px
import android.content.Context
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 9766f9e0ab1a..071d23283c43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -2663,12 +2663,6 @@ public class NotificationStackScrollLayout
}
@Override
- public int getHeadsUpInset() {
- if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0;
- return mHeadsUpInset;
- }
-
- @Override
public int getStackBottomInset() {
return mPaddingBetweenElements + mShelf.getIntrinsicHeight();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 5249a6d304ec..d302fb67dddb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -106,9 +106,6 @@ interface NotificationScrollView {
/** Sets whether the view is displayed in pulsing mode. */
fun setPulsing(pulsing: Boolean, animated: Boolean)
- /** Gets the inset for HUNs when they are not visible */
- fun getHeadsUpInset(): Int
-
/**
* Signals that any open Notification guts should be closed, as scene container is handling
* touch events.
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 aba30d2ea5a5..1474789ea0e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -851,10 +851,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
mLightRevealScrim = lightRevealScrim;
mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
-
- if (PredictiveBackSysUiFlag.isEnabled()) {
- mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
- }
}
private void initBubbles(Bubbles bubbles) {
@@ -3031,9 +3027,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
public void onConfigChanged(Configuration newConfig) {
updateResources();
updateDisplaySize(); // populates mDisplayMetrics
- if (PredictiveBackSysUiFlag.isEnabled()) {
- mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
- }
if (DEBUG) {
Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 6a77988e5feb..a339bc98457e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -47,6 +47,7 @@ import com.android.settingslib.Utils;
import com.android.systemui.battery.BatteryMeterView;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
import com.android.systemui.statusbar.phone.ui.TintedIconManager;
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 6028f1727f52..4c2bfe5ca257 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -64,6 +64,7 @@ import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProv
import com.android.systemui.statusbar.disableflags.DisableStateTracker;
import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
index 2e3f0d0abc0a..1e6a0f8efb98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
@@ -18,6 +18,7 @@
package com.android.systemui.statusbar.phone
import com.android.systemui.CoreStartable
+import com.android.systemui.statusbar.layout.LetterboxBackgroundProvider
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
index ca0c1ac9ce7c..0a285519a6ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
@@ -44,6 +44,7 @@ import com.android.systemui.statusbar.data.model.StatusBarAppearance;
import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore;
import com.android.systemui.statusbar.data.repository.StatusBarModePerDisplayRepository;
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
+import com.android.systemui.statusbar.layout.BoundsPair;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.Compile;
import com.android.systemui.util.kotlin.JavaAdapterKt;
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 ccd1b6c4e266..aa1308931f99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -46,6 +46,7 @@ import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.window.StatusBarWindowStateController
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
deleted file mode 100644
index 74d6ba57a8ef..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
+++ /dev/null
@@ -1,53 +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.statusbar.phone
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the predictive back flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object PredictiveBackSysUiFlag {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_PREDICTIVE_BACK_SYSUI
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.predictiveBackSysui()
-
- /**
- * Called to ensure code is only run when the flag is enabled. This protects users from the
- * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
- * build to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun isUnexpectedlyInLegacyMode() =
- RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
- /**
- * Called to ensure code is only run when the flag is disabled. This will throw an exception if
- * the flag is enabled to ensure that the refactor author catches issues in testing.
- */
- @JvmStatic
- inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
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 3749b96199f6..8443edd6aa87 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,7 +18,6 @@ package com.android.systemui.statusbar.phone;
import static android.view.WindowInsets.Type.navigationBars;
-import static com.android.systemui.Flags.predictiveBackAnimateBouncer;
import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
@@ -328,7 +327,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
private float mQsExpansion;
final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
- private boolean mIsBackAnimationEnabled;
private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
private final ActivityStarter mActivityStarter;
@@ -434,7 +432,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
.map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
mAlternateBouncerInteractor = alternateBouncerInteractor;
mBouncerInteractor = bouncerInteractor;
- mIsBackAnimationEnabled = predictiveBackAnimateBouncer();
mUdfpsOverlayInteractor = udfpsOverlayInteractor;
mActivityStarter = activityStarter;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
@@ -630,7 +627,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
private boolean shouldPlayBackAnimation() {
// Suppress back animation when bouncer shouldn't be dismissed on back invocation.
- return !needsFullscreenBouncer() && mIsBackAnimationEnabled;
+ return !needsFullscreenBouncer();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 03324d2a3e6a..c47ed1722bb4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
-
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
@@ -285,15 +283,13 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
for (int i = 0; i < mOnCreateRunnables.size(); i++) {
mOnCreateRunnables.get(i).run();
}
- if (predictiveBackAnimateDialogs()) {
- View targetView = getWindow().getDecorView();
- DialogKt.registerAnimationOnBackInvoked(
- /* dialog = */ this,
- /* targetView = */ targetView,
- /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
- () -> targetView.getResources().getDisplayMetrics())
- );
- }
+ View targetView = getWindow().getDecorView();
+ DialogKt.registerAnimationOnBackInvoked(
+ /* dialog = */ this,
+ /* targetView = */ targetView,
+ /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
+ () -> targetView.getResources().getDisplayMetrics())
+ );
}
private void updateWindowSize() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
index 5837752abdaf..7207d0aef3ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
@@ -21,13 +21,13 @@ import com.android.systemui.dagger.qualifiers.DisplaySpecific;
import com.android.systemui.dagger.qualifiers.RootView;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider;
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.LegacyLightsOutNotifController;
import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
import com.android.systemui.statusbar.phone.StatusBarDemoMode;
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
import com.android.systemui.statusbar.window.StatusBarWindowController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
index ba9181436fb9..b56a9a18d051 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.phone.fragment.dagger
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
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 12ed647fdee7..fdc2d8d96f9b 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,8 +16,8 @@
package com.android.systemui.statusbar.policy.domain.interactor
-import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
import android.content.Context
+import android.media.AudioManager
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
@@ -29,6 +29,7 @@ import com.android.settingslib.notification.data.repository.ZenModeRepository
import com.android.settingslib.notification.modes.ZenIcon
import com.android.settingslib.notification.modes.ZenIconLoader
import com.android.settingslib.notification.modes.ZenMode
+import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
@@ -67,6 +68,17 @@ constructor(
deviceProvisioningRepository: DeviceProvisioningRepository,
userSetupRepository: UserSetupRepository,
) {
+ /**
+ * List of predicates to determine if the [ZenMode] blocks an audio stream. Typical use case
+ * would be: `zenModeByStreamPredicates[stream](zenMode)`
+ */
+ private val zenModeByStreamPredicates =
+ mapOf<Int, (ZenMode) -> Boolean>(
+ AudioManager.STREAM_MUSIC to { it.policy.priorityCategoryMedia == STATE_DISALLOW },
+ AudioManager.STREAM_ALARM to { it.policy.priorityCategoryAlarms == STATE_DISALLOW },
+ AudioManager.STREAM_SYSTEM to { it.policy.priorityCategorySystem == STATE_DISALLOW },
+ )
+
val isZenAvailable: Flow<Boolean> =
combine(
deviceProvisioningRepository.isDeviceProvisioned,
@@ -125,21 +137,16 @@ 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
- }
+ fun canBeBlockedByZenMode(stream: AudioStream): Boolean =
+ zenModeByStreamPredicates.containsKey(stream.value)
- private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> {
+ fun activeModesBlockingStream(stream: AudioStream): Flow<ActiveZenModes> {
+ val isBlockingStream = zenModeByStreamPredicates[stream.value]
+ require(isBlockingStream != null) {
+ "$stream is unsupported. Use canBeBlockedByZenMode to check if the stream can be affected by the Zen Mode."
+ }
return modes
- .map { modes -> modes.filter { mode -> predicate(mode) } }
+ .map { modes -> modes.filter { isBlockingStream(it) } }
.map { modes -> buildActiveZenModes(modes) }
.flowOn(bgDispatcher)
.distinctUntilChanged()
@@ -194,7 +201,6 @@ constructor(
)
null
}
-
ZEN_DURATION_FOREVER -> null
else -> Duration.ofMinutes(zenDuration.toLong())
}
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 1e043ec48142..ecfcb29a9944 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -23,7 +23,7 @@ import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.fragments.FragmentHostManager
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import java.util.Optional
/** Encapsulates all logic for the status bar window state management. */
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 848e91d6f896..8518acb61f84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -58,7 +58,7 @@ import com.android.systemui.res.R;
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
import com.android.systemui.statusbar.core.StatusBarRootModernization;
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.window.StatusBarWindowModule.InternalWindowViewInflater;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index 67ffb0602860..65a683c626dd 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -82,7 +82,7 @@ constructor(private val viewModel: VolumeDialogSliderViewModel) {
// coerce the current value to the new value range before animating it. This prevents
// animating from the value that is outside of current [valueFrom, valueTo].
value = value.coerceIn(valueFrom, valueTo)
- setTrackIconActiveStart(model.iconRes)
+ trackIconActiveStart = model.icon
if (isInitialUpdate) {
value = model.value
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
index 5c39b6f9359c..daf4c8275d20 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
@@ -16,13 +16,16 @@
package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
import android.media.AudioManager
import androidx.annotation.DrawableRes
-import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -31,11 +34,12 @@ import kotlinx.coroutines.flow.flowOf
class VolumeDialogSliderIconProvider
@Inject
constructor(
- private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
+ private val context: Context,
+ private val zenModeInteractor: ZenModeInteractor,
private val audioVolumeInteractor: AudioVolumeInteractor,
) {
- @DrawableRes
+ @SuppressLint("UseCompatLoadingForDrawables")
fun getStreamIcon(
stream: Int,
level: Int,
@@ -43,54 +47,71 @@ constructor(
levelMax: Int,
isMuted: Boolean,
isRoutedToBluetooth: Boolean,
- ): Flow<Int> {
+ ): Flow<Drawable> {
return combine(
- notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)),
+ zenModeInteractor.activeModesBlockingStream(AudioStream(stream)),
ringerModeForStream(stream),
- ) { isZenMuted, ringerMode ->
- val isStreamOffline = level == 0 || isMuted
- if (isZenMuted) {
- // TODO(b/372466264) use icon for the corresponding zenmode
- return@combine com.android.internal.R.drawable.ic_qs_dnd
- }
- when (ringerMode?.value) {
- AudioManager.RINGER_MODE_VIBRATE ->
- return@combine R.drawable.ic_volume_ringer_vibrate
- AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off
- }
- if (isRoutedToBluetooth) {
- return@combine if (stream == AudioManager.STREAM_VOICE_CALL) {
- R.drawable.ic_volume_bt_sco
- } else {
- if (isStreamOffline) {
- R.drawable.ic_volume_media_bt_mute
- } else {
- R.drawable.ic_volume_media_bt
- }
- }
+ ) { activeModesBlockingStream, ringerMode ->
+ if (activeModesBlockingStream.mainMode?.icon != null) {
+ return@combine activeModesBlockingStream.mainMode.icon.drawable
+ } else {
+ context.getDrawable(
+ getIconRes(
+ stream,
+ level,
+ levelMin,
+ levelMax,
+ isMuted,
+ isRoutedToBluetooth,
+ ringerMode,
+ )
+ )!!
}
+ }
+ }
- return@combine if (isStreamOffline) {
- getMutedIconForStream(stream) ?: getIconForStream(stream)
+ @DrawableRes
+ private fun getIconRes(
+ stream: Int,
+ level: Int,
+ levelMin: Int,
+ levelMax: Int,
+ isMuted: Boolean,
+ isRoutedToBluetooth: Boolean,
+ ringerMode: RingerMode?,
+ ): Int {
+ val isStreamOffline = level == 0 || isMuted
+ when (ringerMode?.value) {
+ AudioManager.RINGER_MODE_VIBRATE -> return R.drawable.ic_volume_ringer_vibrate
+ AudioManager.RINGER_MODE_SILENT -> return R.drawable.ic_ring_volume_off
+ }
+ if (isRoutedToBluetooth) {
+ return if (stream == AudioManager.STREAM_VOICE_CALL) {
+ R.drawable.ic_volume_bt_sco
} else {
- if (level < (levelMax + levelMin) / 2) {
- // This icon is different on TV
- R.drawable.ic_volume_media_low
+ if (isStreamOffline) {
+ R.drawable.ic_volume_media_bt_mute
} else {
- getIconForStream(stream)
+ R.drawable.ic_volume_media_bt
}
}
}
- }
- @DrawableRes
- private fun getMutedIconForStream(stream: Int): Int? {
- return when (stream) {
- AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
- AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
- AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
- AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
- else -> null
+ return if (isStreamOffline) {
+ when (stream) {
+ AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
+ AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
+ AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
+ AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
+ else -> null
+ } ?: getIconForStream(stream)
+ } else {
+ if (level < (levelMax + levelMin) / 2) {
+ // This icon is different on TV
+ R.drawable.ic_volume_media_low
+ } else {
+ getIconForStream(stream)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
index 5750c049082f..8df9e788905c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -16,21 +16,21 @@
package com.android.systemui.volume.dialog.sliders.ui.viewmodel
-import androidx.annotation.DrawableRes
+import android.graphics.drawable.Drawable
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
data class VolumeDialogSliderStateModel(
val minValue: Float,
val maxValue: Float,
val value: Float,
- @DrawableRes val iconRes: Int,
+ val icon: Drawable,
)
-fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel {
+fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel {
return VolumeDialogSliderStateModel(
minValue = levelMin.toFloat(),
value = level.toFloat(),
maxValue = levelMax.toFloat(),
- iconRes = iconRes,
+ icon = icon,
)
}
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 cec3d1eb86f0..5b8d9b045475 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,9 +18,6 @@ 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.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEventLogger
@@ -34,8 +31,6 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
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
@@ -43,12 +38,15 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
@@ -101,48 +99,16 @@ constructor(
)
override val slider: StateFlow<SliderState> =
- 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)
- }
+ combine(
+ audioVolumeInteractor.getAudioStream(audioStream),
+ audioVolumeInteractor.canChangeVolume(audioStream),
+ audioVolumeInteractor.ringerMode,
+ streamDisabledMessage(),
+ ) { model, isEnabled, ringerMode, streamDisabledMessage ->
+ volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
+ model.toState(isEnabled, ringerMode, streamDisabledMessage)
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
init {
volumeChanges
@@ -229,40 +195,32 @@ 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)
+ // 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.
+ private fun streamDisabledMessage(): Flow<String> {
+ return if (ModesUiIcons.isEnabled) {
+ if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
+ flowOf(context.getString(R.string.stream_notification_unavailable))
} else {
- // Should not actually be visible, but as a catch-all.
- context.getString(R.string.stream_unavailable_by_unknown)
+ if (zenModeInteractor.canBeBlockedByZenMode(audioStream)) {
+ zenModeInteractor.activeModesBlockingStream(audioStream).map { blockingZenModes
+ ->
+ blockingZenModes.mainMode?.name?.let {
+ context.getString(R.string.stream_unavailable_by_modes, it)
+ } ?: context.getString(R.string.stream_unavailable_by_unknown)
+ }
+ } else {
+ flowOf(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)
+ flowOf(
+ if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
+ context.getString(R.string.stream_notification_unavailable)
+ } else {
+ context.getString(R.string.stream_alarm_unavailable)
+ }
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 7a3089f33276..7a3089f33276 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.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 7ba797c03a0d..86063acbf2e1 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
@@ -39,7 +39,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.AudioDeviceAttributes;
@@ -1286,13 +1285,6 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
}
@Test
- public void setColorFilter_setColorFilterToDrawable() {
- mMediaSwitchingController.setColorFilter(mDrawable, true);
-
- verify(mDrawable).setColorFilter(any(PorterDuffColorFilter.class));
- }
-
- @Test
public void resetGroupMediaDevices_clearGroupDevices() {
final MediaDevice selectedMediaDevice1 = mock(MediaDevice.class);
final MediaDevice selectedMediaDevice2 = mock(MediaDevice.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 2aa300df4f7c..2aa300df4f7c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index a0ecb802dd61..f695c13a9e62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -76,6 +76,8 @@ class UserTrackerImplTest : SysuiTestCase() {
@Mock private lateinit var iActivityManager: IActivityManager
+ @Mock private lateinit var beforeUserSwitchingReply: IRemoteCallback
+
@Mock private lateinit var userSwitchingReply: IRemoteCallback
@Mock(stubOnly = true) private lateinit var dumpManager: DumpManager
@@ -199,9 +201,10 @@ class UserTrackerImplTest : SysuiTestCase() {
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
+ captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
captor.value.onUserSwitching(newID, userSwitchingReply)
runCurrent()
+ verify(beforeUserSwitchingReply).sendResult(any())
verify(userSwitchingReply).sendResult(any())
verify(userManager).getProfiles(newID)
@@ -341,10 +344,11 @@ class UserTrackerImplTest : SysuiTestCase() {
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
+ captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
captor.value.onUserSwitching(newID, userSwitchingReply)
runCurrent()
+ verify(beforeUserSwitchingReply).sendResult(any())
verify(userSwitchingReply).sendResult(any())
assertThat(callback.calledOnUserChanging).isEqualTo(1)
assertThat(callback.lastUser).isEqualTo(newID)
@@ -395,7 +399,7 @@ class UserTrackerImplTest : SysuiTestCase() {
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
+ captor.value.onBeforeUserSwitching(newID, any())
captor.value.onUserSwitchComplete(newID)
runCurrent()
@@ -453,8 +457,10 @@ class UserTrackerImplTest : SysuiTestCase() {
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
captor.value.onUserSwitching(newID, userSwitchingReply)
runCurrent()
+ verify(beforeUserSwitchingReply).sendResult(any())
verify(userSwitchingReply).sendResult(any())
captor.value.onUserSwitchComplete(newID)
@@ -488,6 +494,7 @@ class UserTrackerImplTest : SysuiTestCase() {
}
private class TestCallback : UserTracker.Callback {
+ var calledOnBeforeUserChanging = 0
var calledOnUserChanging = 0
var calledOnUserChanged = 0
var calledOnProfilesChanged = 0
@@ -495,6 +502,11 @@ class UserTrackerImplTest : SysuiTestCase() {
var lastUserContext: Context? = null
var lastUserProfiles = emptyList<UserInfo>()
+ override fun onBeforeUserSwitching(newUser: Int) {
+ calledOnBeforeUserChanging++
+ lastUser = newUser
+ }
+
override fun onUserChanging(newUser: Int, userContext: Context) {
calledOnUserChanging++
lastUser = newUser
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 4cad5f7378bb..77ca51c5efcb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -36,7 +36,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.ShowingPersistentDot
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.time.FakeSystemClock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index 25138fd0ff83..57a12df0cfee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -123,7 +124,8 @@ class IconManagerTest : SysuiTestCase() {
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testCreateIcons_chipNotifIconFlagEnabled_statusBarChipIconIsNull() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
entry?.let { iconManager.createIcons(it) }
@@ -133,6 +135,17 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
+ entry?.let { iconManager.createIcons(it) }
+ testScope.runCurrent()
+
+ assertThat(entry?.icons?.statusBarChipIcon).isNull()
+ }
+
+ @Test
fun testCreateIcons_importantConversation_shortcutIcon() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
@@ -158,7 +171,7 @@ class IconManagerTest : SysuiTestCase() {
notificationEntry(
hasShortcut = false,
hasMessageSenderIcon = false,
- hasLargeIcon = true
+ hasLargeIcon = true,
)
entry?.channel?.isImportantConversation = true
entry?.let { iconManager.createIcons(it) }
@@ -172,7 +185,7 @@ class IconManagerTest : SysuiTestCase() {
notificationEntry(
hasShortcut = false,
hasMessageSenderIcon = false,
- hasLargeIcon = false
+ hasLargeIcon = false,
)
entry?.channel?.isImportantConversation = true
entry?.let { iconManager.createIcons(it) }
@@ -187,7 +200,7 @@ class IconManagerTest : SysuiTestCase() {
hasShortcut = true,
hasMessageSenderIcon = true,
useMessagingStyle = false,
- hasLargeIcon = true
+ hasLargeIcon = true,
)
entry?.channel?.isImportantConversation = true
entry?.let { iconManager.createIcons(it) }
@@ -205,7 +218,8 @@ class IconManagerTest : SysuiTestCase() {
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testCreateIcons_sensitiveImportantConversation() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
entry?.setSensitive(true, true)
@@ -219,8 +233,24 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+ entry?.setSensitive(true, true)
+ entry?.channel?.isImportantConversation = true
+ entry?.let { iconManager.createIcons(it) }
+ testScope.runCurrent()
+ assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+ assertThat(entry?.icons?.statusBarChipIcon).isNull()
+ assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+ assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+ }
+
+ @Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testUpdateIcons_sensitiveImportantConversation() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
entry?.setSensitive(true, true)
@@ -236,6 +266,23 @@ class IconManagerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+ entry?.setSensitive(true, true)
+ entry?.channel?.isImportantConversation = true
+ entry?.let { iconManager.createIcons(it) }
+ // Updating the icons after creation shouldn't break anything
+ entry?.let { iconManager.updateIcons(it) }
+ testScope.runCurrent()
+ assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+ assertThat(entry?.icons?.statusBarChipIcon).isNull()
+ assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+ assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+ }
+
+ @Test
fun testUpdateIcons_sensitivityChange() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
@@ -254,7 +301,7 @@ class IconManagerTest : SysuiTestCase() {
hasShortcut: Boolean,
hasMessageSenderIcon: Boolean,
useMessagingStyle: Boolean = true,
- hasLargeIcon: Boolean
+ hasLargeIcon: Boolean,
): NotificationEntry? {
val n =
Notification.Builder(mContext, "id")
@@ -270,7 +317,7 @@ class IconManagerTest : SysuiTestCase() {
SystemClock.currentThreadTimeMillis(),
Person.Builder()
.setIcon(if (hasMessageSenderIcon) messageIc else null)
- .build()
+ .build(),
)
)
if (useMessagingStyle) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
index 59ad38a87146..59ad38a87146 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
index 17093291e8b0..2a1877adc172 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
@@ -24,7 +24,6 @@ fun fakeDialogTransitionAnimator(
@Main mainExecutor: Executor,
isUnlocked: Boolean = true,
isShowingAlternateAuthOnUnlock: Boolean = false,
- isPredictiveBackQsDialogAnim: Boolean = false,
interactionJankMonitor: InteractionJankMonitor,
): DialogTransitionAnimator {
return DialogTransitionAnimator(
@@ -35,10 +34,6 @@ fun fakeDialogTransitionAnimator(
isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
),
interactionJankMonitor = interactionJankMonitor,
- featureFlags =
- object : AnimationFeatureFlags {
- override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
- },
transitionAnimator = fakeTransitionAnimator(mainExecutor),
isForTesting = true,
)
@@ -50,6 +45,8 @@ private class FakeCallback(
private val isShowingAlternateAuthOnUnlock: Boolean = false,
) : DialogTransitionAnimator.Callback {
override fun isDreaming(): Boolean = isDreaming
+
override fun isUnlocked(): Boolean = isUnlocked
+
override fun isShowingAlternateAuthOnUnlock() = isShowingAlternateAuthOnUnlock
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index dad85697ef1d..9815da92ebbf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -19,7 +19,6 @@ package com.android.systemui.flags
import android.platform.test.annotations.EnableFlags
import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
-import com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_SYSUI
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
/**
@@ -29,7 +28,6 @@ import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
@EnableFlags(
FLAG_KEYGUARD_WM_STATE_REFACTOR,
FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN,
- FLAG_PREDICTIVE_BACK_SYSUI,
FLAG_SCENE_CONTAINER,
)
@Retention(AnnotationRetention.RUNTIME)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 47991b3b9689..3df3ee983ecf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -154,6 +154,7 @@ val Kosmos.shortcutHelperCoreStartable by
shortcutHelperStateRepository,
activityStarter,
testScope,
+ customInputGesturesRepository
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index 43835607c77d..1881a94c8984 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -1,5 +1,6 @@
package com.android.systemui.kosmos
+import androidx.compose.runtime.snapshots.Snapshot
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
@@ -57,6 +58,23 @@ var Kosmos.brightnessWarningToast: BrightnessWarningToast by
fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) =
testScope.runTest testBody@{ this@runTest.testBody() }
+/**
+ * Runs the given [Kosmos]-scoped test [block] in an environment where compose snapshot state is
+ * settled eagerly. This is the compose equivalent to using an [UnconfinedTestDispatcher] or using
+ * [runCurrent] a lot.
+ *
+ * Note that this shouldn't be needed or used in a compose test environment.
+ */
+fun Kosmos.runTestWithSnapshots(block: suspend Kosmos.() -> Unit) {
+ val handle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() }
+
+ try {
+ testScope.runTest { block() }
+ } finally {
+ handle.dispose()
+ }
+}
+
fun Kosmos.runCurrent() = testScope.runCurrent()
fun <T> Kosmos.collectLastValue(flow: Flow<T>) = testScope.collectLastValue(flow)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 4a86fd5e49ff..74deaab67766 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -21,6 +21,8 @@ import com.android.systemui.dagger.SysUISingleton
import dagger.Binds
import dagger.Module
import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -37,8 +39,8 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository {
override val udfpsTransitionToFullShadeProgress =
_udfpsTransitionToFullShadeProgress.asStateFlow()
- private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
- override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+ override val currentFling: MutableSharedFlow<FlingInfo?> =
+ MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val _lockscreenShadeExpansion = MutableStateFlow(0f)
override val lockscreenShadeExpansion = _lockscreenShadeExpansion.asStateFlow()
@@ -139,7 +141,7 @@ class FakeShadeRepository @Inject constructor() : ShadeRepository {
}
override fun setCurrentFling(info: FlingInfo?) {
- _currentFling.value = info
+ currentFling.tryEmit(info)
}
override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
index 642c2ff38338..67f8572cf7f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.data.repository
import android.view.Display
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import org.mockito.kotlin.mock
class FakeStatusBarContentInsetsProviderStore() : StatusBarContentInsetsProviderStore {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
index a34fb0998c79..af7a4638faf0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
@@ -21,7 +21,7 @@ import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.displayWindowPropertiesRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.statusbar.phone.statusBarContentInsetsProviderFactory
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProviderFactory
import com.android.systemui.sysUICutoutProviderFactory
val Kosmos.fakeStatusBarContentInsetsProviderStore by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
index 4fb8cf4a328b..ad742c887af1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.content.Context
import com.android.systemui.SysUICutoutProvider
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
index 705df3c15d18..69e215dcba6a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import com.android.systemui.kosmos.Kosmos
import org.mockito.kotlin.mock
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
index 7eaecb1c4544..3a19547f0713 100644
--- 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
@@ -19,7 +19,7 @@ package com.android.systemui.statusbar.window
import android.content.Context
import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory {
override fun create(
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 23f2b4221825..f595aef41e2d 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
@@ -22,7 +22,7 @@ import com.android.app.viewcapture.realCaptureAwareWindowManager
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.fragments.fragmentService
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.phone.statusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.statusBarConfigurationController
import java.util.Optional
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
new file mode 100644
index 000000000000..ca02576f7093
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+/** Returns a [State] that is `true` only when all of [states] are `true`. */
+@ExperimentalKairosApi
+fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
+
+/** Returns a [State] that is `true` when any of [states] are `true`. */
+@ExperimentalKairosApi
+fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
+
+/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
+@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
+
+private fun Iterable<Boolean>.allTrue() = all { it }
+
+private fun Iterable<Boolean>.anyTrue() = any { it }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
index b6918703b404..bd2173cd2393 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
@@ -17,17 +17,14 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -36,9 +33,8 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.launch
-/** A function that modifies the KairosNetwork. */
+/** A computation that can modify the Kairos network. */
typealias BuildSpec<A> = BuildScope.() -> A
/**
@@ -56,17 +52,7 @@ inline operator fun <A> BuildScope.invoke(block: BuildScope.() -> A) = run(block
/** Operations that add inputs and outputs to a Kairos network. */
@ExperimentalKairosApi
-interface BuildScope : StateScope {
-
- /**
- * A [KairosNetwork] handle that is bound to this [BuildScope].
- *
- * It supports all of the standard functionality by which external code can interact with this
- * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
- * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children
- * are also destroyed.
- */
- val kairosNetwork: KairosNetwork
+interface BuildScope : HasNetwork, StateScope {
/**
* Defers invoking [block] until after the current [BuildScope] code-path completes, returning a
@@ -110,11 +96,21 @@ interface BuildScope : StateScope {
* executed if this [BuildScope] is still active by that time. It can be deactivated due to a
* -Latest combinator, for example.
*
- * Shorthand for:
- * ```kotlin
- * events.observe { effect { ... } }
+ * [Disposing][DisposableHandle.dispose] of the returned [DisposableHandle] will stop the
+ * observation of new emissions. It will however *not* cancel any running effects from previous
+ * emissions. To achieve this behavior, use [launchScope] or [asyncScope] to create a child
+ * build scope:
+ * ``` kotlin
+ * val job = launchScope {
+ * events.observe { x ->
+ * launchEffect { longRunningEffect(x) }
+ * }
+ * }
+ * // cancels observer and any running effects:
+ * job.cancel()
* ```
*/
+ // TODO: remove disposable handle return? might add more confusion than convenience
fun <A> Events<A>.observe(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: EffectScope.(A) -> Unit = {},
@@ -129,7 +125,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -138,32 +134,32 @@ interface BuildScope : StateScope {
): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
/**
- * Creates an instance of an [Events] with elements that are from [builder].
+ * Creates an instance of an [Events] with elements that are emitted from [builder].
*
* [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableState].
+ * provided [EventProducerScope].
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * events { ... }.apply { observe() }
+ * ``` kotlin
+ * events { ... }.apply { observe() }
* ```
*/
- fun <T> events(
- name: String? = null,
- builder: suspend EventProducerScope<T>.() -> Unit,
- ): Events<T>
+ // TODO: eventually this should be defined on KairosNetwork + an extension on HasNetwork
+ // - will require modifying InputNode so that it can be manually killed, as opposed to using
+ // takeUntil (which requires a StateScope).
+ fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T>
/**
* Creates an instance of an [Events] with elements that are emitted from [builder].
*
* [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableState].
+ * provided [CoalescingEventProducerScope].
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * events { ... }.apply { observe() }
+ * ``` kotlin
+ * events { ... }.apply { observe() }
* ```
*
* In the event of backpressure, emissions are *coalesced* into batches. When a value is
@@ -171,6 +167,7 @@ interface BuildScope : StateScope {
* [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the
* batch is reset back to [getInitialValue].
*/
+ // TODO: see TODO for [events]
fun <In, Out> coalescingEvents(
getInitialValue: () -> Out,
coalesce: (old: Out, new: In) -> Out,
@@ -186,6 +183,7 @@ interface BuildScope : StateScope {
*
* The return value from [block] can be accessed via the returned [DeferredValue].
*/
+ // TODO: return a DisposableHandle instead of Job?
fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job>
// TODO: once we have context params, these can all become extensions:
@@ -198,9 +196,9 @@ interface BuildScope : StateScope {
* outside of the current Kairos transaction; when [transform] returns, the returned value is
* emitted from the result [Events] in a new transaction.
*
- * Shorthand for:
- * ```kotlin
- * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
+ * mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
* ```
*/
fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
@@ -219,42 +217,19 @@ interface BuildScope : StateScope {
/**
* Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
* [value of this State][State.sample], and will emit at the same rate as [State.changes].
- *
- * Note that the [value][StateFlow.value] is not available until the *end* of the current
- * transaction. If you need the current value before this time, then use [State.sample].
*/
fun <A> State<A>.toStateFlow(): StateFlow<A> {
- val uninitialized = Any()
- var initialValue: Any? = uninitialized
- val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
- deferredBuildScope {
- initialValue = sample()
- changes.observe {
- innerStateFlow.value = it
- initialValue = null
- }
- }
-
- @Suppress("UNCHECKED_CAST")
- fun getValue(innerValue: Any?): A =
- when {
- innerValue !== uninitialized -> innerValue as A
- initialValue !== uninitialized -> initialValue as A
- else ->
- error(
- "Attempted to access StateFlow.value before Kairos transaction has completed."
- )
- }
-
+ val innerStateFlow = MutableStateFlow(sampleDeferred())
+ changes.observe { innerStateFlow.value = deferredOf(it) }
return object : StateFlow<A> {
override val replayCache: List<A>
- get() = innerStateFlow.replayCache.map(::getValue)
+ get() = innerStateFlow.replayCache.map { it.value }
override val value: A
- get() = getValue(innerStateFlow.value)
+ get() = innerStateFlow.value.value
override suspend fun collect(collector: FlowCollector<A>): Nothing {
- innerStateFlow.collect { collector.emit(getValue(it)) }
+ innerStateFlow.collect { collector.emit(it.value) }
}
}
}
@@ -365,14 +340,14 @@ interface BuildScope : StateScope {
initialSpec: BuildSpec<A>
): Pair<Events<B>, DeferredValue<A>> {
val (events, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
+ mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
.applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
val outEvents: Events<B> =
events.mapMaybe {
checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
}
val outInit: DeferredValue<A> = deferredBuildScope {
- val initResult: Map<Unit, A> = result.get()
+ val initResult: Map<Unit, A> = result.value
check(Unit in initResult) {
"applyLatest: expected initial result, but none present in: $initResult"
}
@@ -425,7 +400,7 @@ interface BuildScope : StateScope {
transform: BuildScope.(A) -> B,
): Pair<Events<B>, DeferredValue<B>> =
mapCheap { buildSpec { transform(it) } }
- .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) })
+ .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.value) })
/**
* Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
@@ -436,7 +411,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -445,6 +420,17 @@ interface BuildScope : StateScope {
): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+ /**
+ * Returns an [Incremental] containing the results of applying each [BuildSpec] emitted from the
+ * original [Incremental].
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the
+ * same key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
fun <K, V> Incremental<K, BuildSpec<V>>.applyLatestSpecForKey(
numKeys: Int? = null
): Incremental<K, V> {
@@ -460,7 +446,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.applyLatestSpecForKey(
@@ -476,7 +462,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -495,7 +481,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -513,7 +499,7 @@ interface BuildScope : StateScope {
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -524,7 +510,7 @@ interface BuildScope : StateScope {
map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } }
.applyLatestSpecForKey(
deferredBuildScope {
- initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } }
+ initialValues.value.mapValues { (k, v) -> buildSpec { transform(k, v) } }
},
numKeys = numKeys,
)
@@ -539,7 +525,7 @@ interface BuildScope : StateScope {
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -558,7 +544,7 @@ interface BuildScope : StateScope {
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -570,7 +556,7 @@ interface BuildScope : StateScope {
fun <R> Events<R>.nextDeferred(): Deferred<R> {
lateinit var next: CompletableDeferred<R>
val job = launchScope { nextOnly().observe { next.complete(it) } }
- next = CompletableDeferred<R>(parent = job)
+ next = CompletableDeferred(parent = job)
return next
}
@@ -581,12 +567,11 @@ interface BuildScope : StateScope {
}
/** Returns an [Events] that emits whenever this [Flow] emits. */
- fun <A> Flow<A>.toEvents(name: String? = null): Events<A> =
- events(name) { collect { emit(it) } }
+ fun <A> Flow<A>.toEvents(): Events<A> = events { collect { emit(it) } }
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.toEvents().holdState(initialValue)
* ```
*/
@@ -594,7 +579,7 @@ interface BuildScope : StateScope {
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.scan(initialValue, operation).toEvents().holdState(initialValue)
* ```
*/
@@ -603,7 +588,7 @@ interface BuildScope : StateScope {
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue)
* ```
*/
@@ -679,6 +664,13 @@ interface BuildScope : StateScope {
* Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can
* be used to make further modifications to the Kairos network, and/or perform side-effects via
* [effect].
+ *
+ * ``` kotlin
+ * fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
+ * block(sample())
+ * changes.observeBuild(block)
+ * }
+ * ```
*/
fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
block(sample())
@@ -706,12 +698,9 @@ interface BuildScope : StateScope {
* outside of the current Kairos transaction; when it completes, the returned [Events] emits in a
* new transaction.
*
- * Shorthand for:
- * ```
- * events { emitter: MutableEvents<A> ->
- * val a = block()
- * emitter.emit(a)
- * }
+ * ``` kotlin
+ * fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
+ * events { emit(block()) }.apply { observe() }
* ```
*/
@ExperimentalKairosApi
@@ -730,9 +719,12 @@ fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
* executed if this [BuildScope] is still active by that time. It can be deactivated due to a
* -Latest combinator, for example.
*
- * Shorthand for:
- * ```kotlin
- * launchScope { now.observe { block() } }
+ * ``` kotlin
+ * fun BuildScope.effect(
+ * context: CoroutineContext = EmptyCoroutineContext,
+ * block: EffectScope.() -> Unit,
+ * ): Job =
+ * launchScope { now.observe(context) { block() } }
* ```
*/
@ExperimentalKairosApi
@@ -748,13 +740,14 @@ fun BuildScope.effect(
* done because the current [BuildScope] might be deactivated within this transaction, perhaps due
* to a -Latest combinator. If this happens, then the coroutine will never actually be started.
*
- * Shorthand for:
- * ```kotlin
- * effect { effectCoroutineScope.launch { block() } }
+ * ``` kotlin
+ * fun BuildScope.launchEffect(block: suspend KairosScope.() -> Unit): Job =
+ * effect { effectCoroutineScope.launch { block() } }
* ```
*/
@ExperimentalKairosApi
-fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+fun BuildScope.launchEffect(block: suspend KairosCoroutineScope.() -> Unit): Job =
+ asyncEffect(block)
/**
* Launches [block] in a new coroutine, returning the result as a [Deferred].
@@ -764,17 +757,18 @@ fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asy
* to a -Latest combinator. If this happens, then the coroutine will never actually be started.
*
* Shorthand for:
- * ```kotlin
- * CompletableDeferred<R>.apply {
- * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } }
- * }
- * .await()
+ * ``` kotlin
+ * fun <R> BuildScope.asyncEffect(block: suspend KairosScope.() -> R): Deferred<R> =
+ * CompletableDeferred<R>.apply {
+ * effect { effectCoroutineScope.launch { complete(block()) } }
+ * }
+ * .await()
* ```
*/
@ExperimentalKairosApi
-fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+fun <R> BuildScope.asyncEffect(block: suspend KairosCoroutineScope.() -> R): Deferred<R> {
val result = CompletableDeferred<R>()
- val job = effect { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+ val job = effect { launch { result.complete(block()) } }
val handle = job.invokeOnCompletion { result.cancel() }
result.invokeOnCompletion {
handle.dispose()
@@ -795,7 +789,7 @@ fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
* events { ... }.apply { observe() }
* ```
*
@@ -819,7 +813,7 @@ fun <In, Out> BuildScope.coalescingEvents(
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
* events { ... }.apply { observe() }
* ```
*
@@ -837,7 +831,7 @@ fun <T> BuildScope.conflatedEvents(
}
/** Scope for emitting to a [BuildScope.coalescingEvents]. */
-interface CoalescingEventProducerScope<in T> {
+fun interface CoalescingEventProducerScope<in T> {
/**
* Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
* already pending.
@@ -850,7 +844,7 @@ interface CoalescingEventProducerScope<in T> {
}
/** Scope for emitting to a [BuildScope.events]. */
-interface EventProducerScope<in T> {
+fun interface EventProducerScope<in T> {
/**
* Emits a [value] to this [Events], suspending the caller until the Kairos transaction
* containing the emission has completed.
@@ -868,3 +862,11 @@ suspend fun awaitClose(block: () -> Unit): Nothing =
} finally {
block()
}
+
+/**
+ * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
+ * [State] that holds the result of the currently-active [BuildSpec].
+ */
+@ExperimentalKairosApi
+fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
+ rebuildSignal.map { spec }.holdLatestSpec(spec)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
deleted file mode 100644
index c20864648f00..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
+++ /dev/null
@@ -1,278 +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.kairos
-
-import com.android.systemui.kairos.util.These
-import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.channelFlow
-import kotlinx.coroutines.flow.conflate
-
-/**
- * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
- * emission of the original [Events], within the same transaction of the original emission.
- */
-@ExperimentalKairosApi
-fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
- state: State<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, state.sample()) }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
- sampleable: Transactional<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, sampleable.sample()) }
-
-/**
- * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
- * the new value is passed to [transform].
- *
- * Note that [sample] is both more performant, and safer to use with recursive definitions. You will
- * generally want to use it rather than this.
- *
- * @see sample
- */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.samplePromptly(
- state: State<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> =
- sample(state) { a, b -> These.thiz(a to b) }
- .mergeWith(state.changes.map { These.that(it) }) { thiz, that ->
- These.both((thiz as These.This).thiz, (that as These.That).that)
- }
- .mapMaybe { these ->
- when (these) {
- // both present, transform the upstream value and the new value
- is These.Both -> just(transform(these.thiz.first, these.that))
- // no upstream present, so don't perform the sample
- is These.That -> none()
- // just the upstream, so transform the upstream and the old value
- is These.This -> just(transform(these.thiz.first, these.thiz.second))
- }
- }
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
- * transactionally connect to / disconnect from the [Events] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
- * transactionally connect to / disconnect from the [State] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("eventsSpecToColdConflatedFlow")
-fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("stateSpecToColdConflatedFlow")
-fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [Events].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalFlowToColdConflatedFlow")
-fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalStateToColdConflatedFlow")
-fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulFlowToColdConflatedFlow")
-fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulStateToColdConflatedFlow")
-fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
-
-private fun Iterable<Boolean>.allTrue() = all { it }
-
-private fun Iterable<Boolean>.anyTrue() = any { it }
-
-/** Returns a [State] that is `true` only when all of [states] are `true`. */
-@ExperimentalKairosApi
-fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
-
-/** Returns a [State] that is `true` when any of [states] are `true`. */
-@ExperimentalKairosApi
-fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
-
-/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
-@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
- * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
- * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
- * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
- *
- * Use [compiledBuildSpec] to compile and stand-up a mode graph.
- *
- * @see StatefulMode
- */
-@ExperimentalKairosApi
-fun interface BuildMode<out A> {
- /**
- * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
- * new mode.
- */
- fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
-}
-
-/**
- * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
- * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
- get() = buildSpec {
- var modeChangeEvents by EventsLoop<BuildMode<A>>()
- val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
- modeChangeEvents
- .map { it.run { buildSpec { enableMode() } } }
- .holdLatestSpec(buildSpec { enableMode() })
- modeChangeEvents =
- activeMode
- .map { statefully { it.second.nextOnly() } }
- .applyLatestStateful()
- .switchEvents()
- activeMode.map { it.first }
- }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all state accumulation is immediately started. When the returned
- * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
- * accumulation in the process.
- *
- * Use [compiledStateful] to compile and stand-up a mode graph.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-fun interface StatefulMode<out A> {
- /**
- * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
- * new mode.
- */
- fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
-}
-
-/**
- * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a
- * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as
- * they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
- get() = statefully {
- var modeChangeEvents by EventsLoop<StatefulMode<A>>()
- val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
- modeChangeEvents
- .map { it.run { statefully { enableMode() } } }
- .holdLatestStateful(statefully { enableMode() })
- modeChangeEvents =
- activeMode
- .map { statefully { it.second.nextOnly() } }
- .applyLatestStateful()
- .switchEvents()
- activeMode.map { it.first }
- }
-
-/**
- * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
- * [State] that holds the result of the currently-active [BuildSpec].
- */
-@ExperimentalKairosApi
-fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
- rebuildSignal.map { spec }.holdLatestSpec(spec)
-
-/**
- * Like [changes] but also includes the old value of this [State].
- *
- * Shorthand for:
- * ``` kotlin
- * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
- * ```
- */
-@ExperimentalKairosApi
-val <A> State<A>.transitions: Events<WithPrev<A, A>>
- get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
new file mode 100644
index 000000000000..b3d89c31619b
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.zipStates
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.combineState
+ */
+@ExperimentalKairosApi
+@JvmName(name = "stateCombine")
+fun <A, B, C> State<A>.combine(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
+ combine(this, other, transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<State<A>>.combine(): State<List<A>> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ val states = map { it.init }
+ zipStates(
+ name,
+ operatorName,
+ states.size,
+ states = init(null) { states.map { it.connect(this) } },
+ )
+ }
+ )
+}
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> =
+ asIterable().map { (k, state) -> state.map { v -> k to v } }.combine().map { it.toMap() }
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
+ combine().map(transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [State]s into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
+ states.asIterable().combine(transform)
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ transform: KairosScope.(A, B) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
+ NoScope.transform(a, b)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ transform: KairosScope.(A, B, C) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
+ NoScope.transform(a, b, c)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ transform: KairosScope.(A, B, C, D) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
+ a,
+ b,
+ c,
+ d ->
+ NoScope.transform(a, b, c, d)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, E, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ stateE: State<E>,
+ transform: KairosScope.(A, B, C, D, E) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(
+ name,
+ operatorName,
+ stateA.init,
+ stateB.init,
+ stateC.init,
+ stateD.init,
+ stateE.init,
+ ) { a, b, c, d, e ->
+ NoScope.transform(a, b, c, d, e)
+ }
+ }
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
new file mode 100644
index 000000000000..4b9bb0ee30a2
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.CompletableLazy
+
+/**
+ * A value that may not be immediately (synchronously) available, but is guaranteed to be available
+ * before this transaction is completed.
+ */
+@ExperimentalKairosApi
+class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) {
+ /**
+ * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is
+ * not yet available.
+ */
+ val value: A
+ get() = unwrapped.value
+}
+
+/** Returns an already-available [DeferredValue] containing [value]. */
+@ExperimentalKairosApi
+fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
index 7e257f2831af..14d45d447c54 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
@@ -16,33 +16,46 @@
package com.android.systemui.kairos
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
/**
- * Scope for external side-effects triggered by the Kairos network. This still occurs within the
- * context of a transaction, so general suspending calls are disallowed to prevent blocking the
- * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new
- * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the
- * containing [BuildScope] that this side-effect scope is running in.
+ * Scope for external side-effects triggered by the Kairos network.
+ *
+ * This still occurs within the context of a transaction, so general suspending calls are disallowed
+ * to prevent blocking the transaction. You can [launch] new coroutines to perform long-running
+ * asynchronous work. These coroutines are kept alive for the duration of the containing
+ * [BuildScope] that this side-effect scope is running in.
*/
@ExperimentalKairosApi
-interface EffectScope : TransactionScope {
+interface EffectScope : HasNetwork, TransactionScope {
/**
- * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is
- * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is
- * cancelled.
+ * Creates a coroutine that is a child of this [EffectScope], and returns its future result as a
+ * [Deferred].
+ *
+ * @see kotlinx.coroutines.async
*/
- @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope
+ fun <R> async(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend KairosCoroutineScope.() -> R,
+ ): Deferred<R>
/**
- * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos
- * network.
+ * Launches a new coroutine that is a child of this [EffectScope] without blocking the current
+ * thread and returns a reference to the coroutine as a [Job].
*
- * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope]
- * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any
- * modifications to the Kairos network performed via this [KairosNetwork] instance will be
- * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending
- * [side-effects][BuildScope.effect] are cancelled).
+ * @see kotlinx.coroutines.launch
*/
- @ExperimentalKairosApi val kairosNetwork: KairosNetwork
+ fun launch(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend KairosCoroutineScope.() -> Unit,
+ ): Job = async(context, start, block)
}
+
+@ExperimentalKairosApi interface KairosCoroutineScope : HasNetwork, CoroutineScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
index e7d0096f2189..8f468c153743 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
@@ -17,7 +17,6 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DemuxImpl
import com.android.systemui.kairos.internal.EventsImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
@@ -27,22 +26,11 @@ import com.android.systemui.kairos.internal.NoScope
import com.android.systemui.kairos.internal.activated
import com.android.systemui.kairos.internal.cached
import com.android.systemui.kairos.internal.constInit
-import com.android.systemui.kairos.internal.demuxMap
-import com.android.systemui.kairos.internal.filterImpl
-import com.android.systemui.kairos.internal.filterJustImpl
import com.android.systemui.kairos.internal.init
import com.android.systemui.kairos.internal.mapImpl
-import com.android.systemui.kairos.internal.mergeNodes
-import com.android.systemui.kairos.internal.mergeNodesLeft
import com.android.systemui.kairos.internal.neverImpl
-import com.android.systemui.kairos.internal.switchDeferredImplSingle
-import com.android.systemui.kairos.internal.switchPromptImplSingle
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.toMaybe
import java.util.concurrent.atomic.AtomicReference
import kotlin.reflect.KProperty
@@ -51,7 +39,16 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
-/** A series of values of type [A] available at discrete points in time. */
+/**
+ * A series of values of type [A] available at discrete points in time.
+ *
+ * [Events] follow these rules:
+ * 1. Within a single Kairos network transaction, an [Events] instance will only emit *once*.
+ * 2. The order that different [Events] instances emit values within a transaction is undefined, and
+ * are conceptually *simultaneous*.
+ * 3. [Events] emissions are *ephemeral* and do not last beyond the transaction they are emitted,
+ * unless explicitly [observed][BuildScope.observe] or [held][StateScope.holdState] as a [State].
+ */
@ExperimentalKairosApi
sealed class Events<out A> {
companion object {
@@ -67,7 +64,9 @@ sealed class Events<out A> {
* A forward-reference to an [Events]. Useful for recursive definitions.
*
* This reference can be used like a standard [Events], but will throw an error if its [loopback] is
- * unset before the end of the first transaction which accesses it.
+ * unset before it is [observed][BuildScope.observe].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.eventsLoop
*/
@ExperimentalKairosApi
class EventsLoop<A> : Events<A>() {
@@ -76,7 +75,10 @@ class EventsLoop<A> : Events<A>() {
internal val init: Init<EventsImpl<A>> =
init(name = null) { deferred.value.init.connect(evalScope = this) }
- /** The [Events] this reference is referring to. */
+ /**
+ * The [Events] this reference is referring to. Must be set before this [EventsLoop] is
+ * [observed][BuildScope.observe].
+ */
var loopback: Events<A>? = null
set(value) {
value?.let {
@@ -102,6 +104,12 @@ class EventsLoop<A> : Events<A>() {
* will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Events<A>>.defer() = deferredEvents { value }
+ * ```
+ *
+ * @see deferredEvents
*/
@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value }
@@ -113,6 +121,12 @@ class EventsLoop<A> : Events<A>() {
* and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() }
+ * ```
+ *
+ * @see deferredEvents
*/
@ExperimentalKairosApi
fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value }
@@ -130,25 +144,27 @@ fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInl
NoScope.block()
}
-/** Returns an [Events] that emits the new value of this [State] when it changes. */
-@ExperimentalKairosApi
-val <A> State<A>.changes: Events<A>
- get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
-
/**
- * Returns an [Events] that contains only the [just] results of applying [transform] to each value
- * of the original [Events].
+ * Returns an [Events] that contains only the
+ * [present][com.android.systemui.kairos.util.Maybe.present] results of applying [transform] to each
+ * value of the original [Events].
*
+ * @sample com.android.systemui.kairos.KairosSamples.mapMaybe
* @see mapNotNull
*/
@ExperimentalKairosApi
fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> =
- map(transform).filterJust()
+ map(transform).filterPresent()
/**
* Returns an [Events] that contains only the non-null results of applying [transform] to each value
* of the original [Events].
*
+ * ``` kotlin
+ * fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> =
+ * mapMaybe { if (it == null) absent else present(it) }
+ * ```
+ *
* @see mapMaybe
*/
@ExperimentalKairosApi
@@ -156,23 +172,11 @@ fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B
transform(it).toMaybe()
}
-/** Returns an [Events] containing only values of the original [Events] that are not null. */
-@ExperimentalKairosApi
-fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust()
-
-/** Shorthand for `mapNotNull { it as? A }`. */
-@ExperimentalKairosApi
-inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
- mapCheap { it as? A }.filterNotNull()
-
-/** Shorthand for `mapMaybe { it }`. */
-@ExperimentalKairosApi
-fun <A> Events<Maybe<A>>.filterJust(): Events<A> =
- EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) }))
-
/**
* Returns an [Events] containing the results of applying [transform] to each value of the original
* [Events].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapEvents
*/
@ExperimentalKairosApi
fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
@@ -184,6 +188,7 @@ fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
* Like [map], but the emission is not cached during the transaction. Use only if [transform] is
* fast and pure.
*
+ * @sample com.android.systemui.kairos.KairosSamples.mapCheap
* @see map
*/
@ExperimentalKairosApi
@@ -196,8 +201,9 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> =
* Returns an [Events] that invokes [action] before each value of the original [Events] is emitted.
* Useful for logging and debugging.
*
- * ```
- * pulse.onEach { foo(it) } == pulse.map { foo(it); it }
+ * ``` kotlin
+ * fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> =
+ * map { it.also { action(it) } }
* ```
*
* Note that the side effects performed in [onEach] are only performed while the resulting [Events]
@@ -207,29 +213,19 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> =
*/
@ExperimentalKairosApi
fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map {
- action(it)
- it
-}
-
-/**
- * Returns an [Events] containing only values of the original [Events] that satisfy the given
- * [predicate].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
- val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
- return EventsInit(constInit(name = null, pulse))
+ it.also { action(it) }
}
/**
* Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of
* the original.
*
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ * fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
+ * val lefts = map { it.first }
+ * val rights = map { it.second }
+ * return lefts to rights
+ * }
* ```
*/
@ExperimentalKairosApi
@@ -240,246 +236,6 @@ fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
}
/**
- * Merges the given [Events] into a single [Events] that emits events from both.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.mergeWith(
- other: Events<A>,
- name: String? = null,
- transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
-): Events<A> {
- val node =
- mergeNodes(
- name = name,
- getPulse = { init.connect(evalScope = this) },
- getOther = { other.init.connect(evalScope = this) },
- ) { a, b ->
- transformCoincidence(a, b)
- }
- return EventsInit(constInit(name = null, node))
-}
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-// TODO: can be optimized to avoid creating the intermediate list
-fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
- merge(*events).map { l -> l.reduce(transformCoincidence) }
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
- EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
- EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [Map], and are given the same key of the associated [Events] in the
- * input [Map].
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
- asSequence()
- .map { (k, events) -> events.map { a -> k to a } }
- .toList()
- .merge()
- .map { it.toMap() }
-
-/**
- * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
- * downstream [Events].
- *
- * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
- * value will be emitted from. These downstream [Events] can be obtained via
- * [GroupedEvents.eventsForKey].
- *
- * An example:
- * ```
- * val fooEvents: Events<Map<String, Foo>> = ...
- * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
- * val fooBar: Events<Foo> = fooById["bar"]
- * ```
- *
- * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
- * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
- * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
- * the appropriate downstream [Events], and so operates in `O(1)`.
- *
- * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see selector
- */
-@ExperimentalKairosApi
-fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
- GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
-
-/**
- * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-fun <K, A> Events<A>.groupBy(
- numKeys: Int? = null,
- extractKey: TransactionScope.(A) -> K,
-): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
-
-/**
- * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
- * [predicate].
- *
- * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
- * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.partition(
- predicate: TransactionScope.(A) -> Boolean
-): Pair<Events<A>, Events<A>> {
- val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
- return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
-}
-
-/**
- * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
- * [Left] values, and [Pair.second] will contain [Right] values.
- *
- * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
- * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the
- * [filterIsInstance] check is only performed once per element.
- */
-@ExperimentalKairosApi
-fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
- val (left, right) = partition { it is Left }
- return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value })
-}
-
-/**
- * A mapping from keys of type [K] to [Events] emitting values of type [A].
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
- /**
- * Returns an [Events] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
-
- /**
- * Returns an [Events] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- operator fun get(key: K): Events<A> = eventsForKey(key)
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch does take effect until the *next* transaction after [State] changes. For a switch
- * that takes effect immediately, see [switchEventsPromptly].
- */
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
- return EventsInit(
- constInit(
- name = null,
- switchDeferredImplSingle(
- name = name,
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch takes effect immediately within the same transaction that [State] changes. In
- * general, you should prefer [switchEvents] over this method. It is both safer and more performant.
- */
-// TODO: parameter to handle coincidental emission from both old and new
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
- return EventsInit(
- constInit(
- name = null,
- switchPromptImplSingle(
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
* A mutable [Events] that provides the ability to [emit] values to the network, handling
* backpressure by coalescing all emissions into batches.
*
@@ -494,7 +250,7 @@ internal constructor(
private val getInitialValue: () -> Out,
internal val impl: InputNode<Out> = InputNode(),
) : Events<Out>() {
- internal val storage = AtomicReference(false to lazy { getInitialValue() })
+ private val storage = AtomicReference(false to lazy { getInitialValue() })
override fun toString(): String = "${this::class.simpleName}@$hashString"
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
new file mode 100644
index 000000000000..8ca5ac8652db
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.filterImpl
+import com.android.systemui.kairos.internal.filterPresentImpl
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.toMaybe
+
+/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are not null.
+ *
+ * ``` kotlin
+ * fun <A> Events<A?>.filterNotNull(): Events<A> = mapNotNull { it }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterPresent()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are instances of [A].
+ *
+ * ``` kotlin
+ * inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ * mapNotNull { it as? A }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ mapCheap { it as? A }.filterNotNull()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are present.
+ *
+ * ``` kotlin
+ * fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = mapMaybe { it }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<Maybe<A>>.filterPresent(): Events<A> =
+ EventsInit(constInit(name = null, filterPresentImpl { init.connect(evalScope = this) }))
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that satisfy the given
+ * [predicate].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ * mapMaybe { if (predicate(it)) present(it) else absent }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
+ val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
+ return EventsInit(constInit(name = null, pulse))
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
new file mode 100644
index 000000000000..45da34ac9ae6
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.DemuxImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.demuxMap
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
+import com.android.systemui.kairos.util.orError
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events].
+ *
+ * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
+ * value will be emitted from. These downstream [Events] can be obtained via
+ * [GroupedEvents.eventsForKey].
+ *
+ * An example:
+ * ```
+ * val fooEvents: Events<Map<String, Foo>> = ...
+ * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
+ * val fooBar: Events<Foo> = fooById["bar"]
+ * ```
+ *
+ * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
+ * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
+ * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
+ * the appropriate downstream [Events], and so operates in `O(1)`.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal [HashMap].
+ *
+ * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.groupByKey
+ * @see selector
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
+ GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events]. The downstream [Events] are associated with a [key][K], which is derived
+ * from each emission of the original [Events] via [extractKey].
+ *
+ * ``` kotlin
+ * fun <K, A> Events<A>.groupBy(
+ * numKeys: Int? = null,
+ * extractKey: TransactionScope.(A) -> K,
+ * ): GroupedEvents<K, A> =
+ * map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+ * ```
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<A>.groupBy(
+ numKeys: Int? = null,
+ extractKey: TransactionScope.(A) -> K,
+): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+
+/**
+ * A mapping from keys of type [K] to [Events] emitting values of type [A].
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
+ /**
+ * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
+
+ /**
+ * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ operator fun get(key: K): Events<A> = eventsForKey(key)
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
+ * [predicate].
+ *
+ * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
+ * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.partition(
+ * predicate: TransactionScope.(A) -> Boolean
+ * ): Pair<Events<A>, Events<A>> =
+ * map { if (predicate(it)) left(it) else right(it) }.partitionEither()
+ * ```
+ *
+ * @see partitionEither
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.partition(
+ predicate: TransactionScope.(A) -> Boolean
+): Pair<Events<A>, Events<A>> {
+ val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
+ return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [First] values, and [Pair.second] will contain [Second] values.
+ *
+ * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
+ * [First]s and once for [Second]s, but is slightly more efficient; specifically, the
+ * [filterIsInstance] check is only performed once per element.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> =
+ * map { it.asThese() }.partitionThese()
+ * ```
+ *
+ * @see partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
+ val (left, right) = partition { it is Either.First }
+ return Pair(
+ left.mapCheap { (it as Either.First).value },
+ right.mapCheap { (it as Either.Second).value },
+ )
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [These.first] values, and [Pair.second] will contain [These.second] values. If the original
+ * emission was a [These.both], then both result [Events] will emit a value simultaneously.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<These<A, B>>.partitionThese(): Pair<Events<A>, Events<B>> {
+ val grouped =
+ mapCheap {
+ when (it) {
+ is These.Both -> mapOf(true to it, false to it)
+ is These.Second -> mapOf(false to it)
+ is These.First -> mapOf(true to it)
+ }
+ }
+ .groupByKey(numKeys = 2)
+ return Pair(
+ grouped.eventsForKey(true).mapCheap {
+ it.maybeFirst().orError { "unexpected missing value" }
+ },
+ grouped.eventsForKey(false).mapCheap {
+ it.maybeSecond().orError { "unexpected missing value" }
+ },
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
index c95b9e83594f..d88ae3b81349 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
@@ -21,27 +21,31 @@ import com.android.systemui.kairos.internal.IncrementalImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
import com.android.systemui.kairos.internal.NoScope
-import com.android.systemui.kairos.internal.awaitValues
import com.android.systemui.kairos.internal.constIncremental
import com.android.systemui.kairos.internal.constInit
import com.android.systemui.kairos.internal.init
-import com.android.systemui.kairos.internal.mapImpl
import com.android.systemui.kairos.internal.mapValuesImpl
-import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
-import com.android.systemui.kairos.internal.switchDeferredImpl
-import com.android.systemui.kairos.internal.switchPromptImpl
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.util.MapPatch
-import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.mapPatchFromFullDiff
import kotlin.reflect.KProperty
-/** A [State] tracking a [Map] that receives incremental updates. */
+/**
+ * A [State] tracking a [Map] that receives incremental updates.
+ *
+ * [Incremental] allows one to react to the [subset of changes][updates] to the held map, without
+ * having to perform a manual diff of the map to determine what changed.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ */
sealed class Incremental<K, out V> : State<Map<K, V>>() {
abstract override val init: Init<IncrementalImpl<K, V>>
}
-/** An [Incremental] that never changes. */
+/**
+ * Returns a constant [Incremental] that never changes. [changes] and [updates] are both equivalent
+ * to [emptyEvents], and [TransactionScope.sample] will always produce [value].
+ */
@ExperimentalKairosApi
fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> {
val operatorName = "stateOf"
@@ -57,6 +61,10 @@ fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> {
* [value][Lazy.value] will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Incremental<K, V>>.defer() = deferredIncremental { value }
+ * ```
*/
@ExperimentalKairosApi
fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { value }
@@ -69,6 +77,10 @@ fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { va
* queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Incremental<K, V>>.defer() = deferredIncremental { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <K, V> DeferredValue<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline {
@@ -119,94 +131,14 @@ fun <K, V, U> Incremental<K, V>.mapValues(
}
/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
- * map { it.merge() }.switchEvents()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
- val operatorName = "mergeEventsIncrementally"
- val name = operatorName
- val patches =
- mapImpl({ init.connect(this).patches }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
- }
- return EventsInit(
- constInit(
- name,
- switchDeferredImpl(
- name = name,
- getStorage = {
- init
- .connect(this)
- .getCurrentWithEpoch(this)
- .first
- .mapValues { (_, events) -> events.init.connect(this) }
- .asIterable()
- },
- getPatches = { patches },
- storeFactory = ConcurrentHashMapK.Factory(),
- )
- .awaitValues(),
- )
- )
-}
-
-/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
+ * A forward-reference to an [Incremental]. Useful for recursive definitions.
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
- * map { it.merge() }.switchEventsPromptly()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
+ * This reference can be used like a standard [Incremental], but will throw an error if its
+ * [loopback] is unset before it is [observed][BuildScope.observe] or
+ * [sampled][TransactionScope.sample]. Note that it is safe to invoke
+ * [TransactionScope.sampleDeferred] before [loopback] is set, provided the [DeferredValue] is not
+ * [queried][KairosScope.get].
*/
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
- val operatorName = "mergeEventsIncrementally"
- val name = operatorName
- val patches =
- mapImpl({ init.connect(this).patches }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
- }
- return EventsInit(
- constInit(
- name,
- switchPromptImpl(
- name = name,
- getStorage = {
- init
- .connect(this)
- .getCurrentWithEpoch(this)
- .first
- .mapValues { (_, events) -> events.init.connect(this) }
- .asIterable()
- },
- getPatches = { patches },
- storeFactory = ConcurrentHashMapK.Factory(),
- )
- .awaitValues(),
- )
- )
-}
-
-/** A forward-reference to an [Incremental], allowing for recursive definitions. */
@ExperimentalKairosApi
class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V>() {
@@ -215,7 +147,10 @@ class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V
override val init: Init<IncrementalImpl<K, V>> =
init(name) { deferred.value.init.connect(evalScope = this) }
- /** The [Incremental] this [IncrementalLoop] will forward to. */
+ /**
+ * The [Incremental] this reference is referring to. Must be set before this [IncrementalLoop]
+ * is [observed][BuildScope.observe].
+ */
var loopback: Incremental<K, V>? = null
set(value) {
value?.let {
@@ -237,8 +172,8 @@ class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V
}
/**
- * Returns an [Incremental] whose [updates] are calculated by diffing the given [State]'s
- * [transitions].
+ * Returns an [Incremental] whose [updates] are calculated by [diffing][mapPatchFromFullDiff] the
+ * given [State]'s [transitions].
*/
fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> {
if (this is Incremental<K, V>) return this
@@ -264,34 +199,6 @@ fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> {
)
}
-/** Returns an [Incremental] that acts like the current value of the given [State]. */
-fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
- val stateChangePatches =
- transitions.mapNotNull { (old, new) ->
- mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
- }
- val innerChanges =
- map { inner ->
- merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
- switchPatch + upcomingPatch
- }
- }
- .switchEventsPromptly()
- val flattened = flatten()
- return IncrementalInit(
- init("switchIncremental") {
- val upstream = flattened.init.connect(this)
- IncrementalImpl(
- "switchIncremental",
- "switchIncremental",
- upstream.changes,
- innerChanges.init.connect(this),
- upstream.store,
- )
- }
- )
-}
-
private inline fun <K, V> deferInline(
crossinline block: InitScope.() -> Incremental<K, V>
): Incremental<K, V> = IncrementalInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
index 77598b30658a..19e3fcdb7b06 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
@@ -23,16 +23,15 @@ import com.android.systemui.kairos.internal.util.awaitCancellationAndThen
import com.android.systemui.kairos.internal.util.childScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
-/**
- * Marks declarations that are still **experimental** and shouldn't be used in general production
- * code.
- */
+/** Marks APIs that are still **experimental** and shouldn't be used in general production code. */
@RequiresOptIn(
message = "This API is experimental and should not be used in general production code."
)
@@ -139,37 +138,46 @@ internal class LocalNetwork(
private val endSignal: Events<Any>,
) : KairosNetwork {
override suspend fun <R> transact(block: TransactionScope.() -> R): R =
- network.transaction("KairosNetwork.transact") { block() }.await()
+ network.transaction("KairosNetwork.transact") { block() }.awaitOrCancel()
override suspend fun activateSpec(spec: BuildSpec<*>) {
- val stopEmitter =
- CoalescingMutableEvents(
- name = "activateSpec",
- coalesce = { _, _: Unit -> },
- network = network,
- getInitialValue = {},
- )
- val job =
- network
- .transaction("KairosNetwork.activateSpec") {
- val buildScope =
- BuildScopeImpl(
- stateScope =
- StateScopeImpl(
- evalScope = this,
- endSignal = mergeLeft(stopEmitter, endSignal),
- ),
- coroutineScope = scope,
- )
- buildScope.launchScope(spec)
+ val stopEmitter = conflatedMutableEvents<Unit>()
+ network
+ .transaction("KairosNetwork.activateSpec") {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope =
+ StateScopeImpl(
+ evalScope = this,
+ endSignalLazy = lazy { mergeLeft(stopEmitter, endSignal) },
+ ),
+ coroutineScope = scope,
+ )
+ buildScope.launchScope {
+ spec.applySpec()
+ launchEffect { awaitCancellationAndThen { stopEmitter.emit(Unit) } }
}
- .await()
- awaitCancellationAndThen {
- stopEmitter.emit(Unit)
- job.cancel()
- }
+ }
+ .awaitOrCancel()
+ .joinOrCancel()
}
+ private suspend fun <T> Deferred<T>.awaitOrCancel(): T =
+ try {
+ await()
+ } catch (ex: CancellationException) {
+ cancel(ex)
+ throw ex
+ }
+
+ private suspend fun Job.joinOrCancel(): Unit =
+ try {
+ join()
+ } catch (ex: CancellationException) {
+ cancel(ex)
+ throw ex
+ }
+
override fun <In, Out> coalescingMutableEvents(
coalesce: (old: Out, new: In) -> Out,
getInitialValue: () -> Out,
@@ -214,3 +222,45 @@ fun CoroutineScope.launchKairosNetwork(
scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() }
return RootKairosNetwork(network, scope, scope.coroutineContext.job)
}
+
+@ExperimentalKairosApi
+interface HasNetwork : KairosScope {
+ /**
+ * A [KairosNetwork] handle that is bound to the lifetime of a [BuildScope].
+ *
+ * It supports all of the standard functionality by which external code can interact with this
+ * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
+ * children to the [BuildScope], such that when the [BuildScope] is destroyed, all children are
+ * also destroyed.
+ */
+ val kairosNetwork: KairosNetwork
+}
+
+/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableEvents(): MutableEvents<T> = MutableEvents(kairosNetwork)
+
+/** Returns a [MutableState] with initial state [initialValue]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableState(initialValue: T): MutableState<T> =
+ MutableState(kairosNetwork, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableEvents<In, Out> = CoalescingMutableEvents(kairosNetwork, coalesce, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+): CoalescingMutableEvents<In, Out> =
+ CoalescingMutableEvents(kairosNetwork, coalesce, getInitialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.ConflatedMutableEvents(): CoalescingMutableEvents<T, T> =
+ ConflatedMutableEvents(kairosNetwork)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
index ce3e9235efa8..e526f4530645 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
@@ -16,42 +16,11 @@
package com.android.systemui.kairos
-import com.android.systemui.kairos.internal.CompletableLazy
-
/** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */
@DslMarker annotation class KairosScopeMarker
/**
- * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in
+ * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from inner
* lambdas.
*/
-@KairosScopeMarker
-@ExperimentalKairosApi
-interface KairosScope {
- /** Returns the value held by the [DeferredValue], suspending until available if necessary. */
- fun <A> DeferredValue<A>.get(): A = unwrapped.value
-}
-
-/**
- * A value that may not be immediately (synchronously) available, but is guaranteed to be available
- * before this transaction is completed.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi
-class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>)
-
-/**
- * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not
- * yet available.
- *
- * This API is not meant for general usage within the Kairos network. It is made available mainly
- * for debugging and logging. You should always prefer [get][KairosScope.get] if possible.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value
-
-/** Returns an already-available [DeferredValue] containing [value]. */
-@ExperimentalKairosApi
-fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
+@KairosScopeMarker @ExperimentalKairosApi interface KairosScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
new file mode 100644
index 000000000000..de9dca43b5d5
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.awaitValues
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.mergeNodes
+import com.android.systemui.kairos.internal.mergeNodesLeft
+import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
+import com.android.systemui.kairos.internal.switchDeferredImpl
+import com.android.systemui.kairos.internal.switchPromptImpl
+import com.android.systemui.kairos.util.map
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from both.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.mergeWith(
+ * other: Events<A>,
+ * transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+ * ): Events<A> =
+ * listOf(this, other).merge().map { it.reduce(transformCoincidence) }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.mergeWith(
+ other: Events<A>,
+ transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+): Events<A> {
+ val node =
+ mergeNodes(
+ getPulse = { init.connect(evalScope = this) },
+ getOther = { other.init.connect(evalScope = this) },
+ ) { a, b ->
+ transformCoincidence(a, b)
+ }
+ return EventsInit(constInit(name = null, node))
+}
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ * fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * ``` kotlin
+ * fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ * merge(*events).map { l -> l.reduce(transformCoincidence) }
+ * ```
+ */
+fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ merge(*events).map { l -> l.reduce(transformCoincidence) }
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.merge
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
+ EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * Semantically equivalent to the following definition:
+ * ``` kotlin
+ * fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ * merge().mapCheap { it.first() }
+ * ```
+ *
+ * In reality, the implementation avoids allocating the intermediate list of all coincident
+ * emissions.
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ * fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ */
+@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [Map], and are given the same key of the associated [Events] in the
+ * input [Map].
+ *
+ * ``` kotlin
+ * fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ * asSequence()
+ * .map { (k, events) -> events.map { a -> k to a } }
+ * .toList()
+ * .merge()
+ * .map { it.toMap() }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ asSequence()
+ .map { (k, events) -> events.map { a -> k to a } }
+ .toList()
+ .merge()
+ .map { it.toMap() }
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
+ * map { it.merge() }.switchEvents()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementally
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
+ val operatorName = "mergeEventsIncrementally"
+ val name = operatorName
+ val patches =
+ mapImpl({ init.connect(this).patches }) { patch, _ ->
+ patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+ }
+ return EventsInit(
+ constInit(
+ name,
+ switchDeferredImpl(
+ name = name,
+ getStorage = {
+ init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValues { (_, events) -> events.init.connect(this) }
+ .asIterable()
+ },
+ getPatches = { patches },
+ storeFactory = ConcurrentHashMapK.Factory(),
+ )
+ .awaitValues(),
+ )
+ )
+}
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
+ * map { it.merge() }.switchEventsPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementallyPromptly
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
+ val operatorName = "mergeEventsIncrementallyPromptly"
+ val name = operatorName
+ val patches =
+ mapImpl({ init.connect(this).patches }) { patch, _ ->
+ patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+ }
+ return EventsInit(
+ constInit(
+ name,
+ switchPromptImpl(
+ name = name,
+ getStorage = {
+ init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValues { (_, events) -> events.init.connect(this) }
+ .asIterable()
+ },
+ getPatches = { patches },
+ storeFactory = ConcurrentHashMapK.Factory(),
+ )
+ .awaitValues(),
+ )
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
new file mode 100644
index 000000000000..6c070a65d5a0
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
+ * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
+ * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
+ * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
+ *
+ * Use [compiledBuildSpec] to compile and stand-up a mode graph.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+fun interface BuildMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+ * new mode.
+ */
+ fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
+}
+
+/**
+ * Returns a [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
+ * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
+ get() = buildSpec {
+ var modeChangeEvents by EventsLoop<BuildMode<A>>()
+ val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { buildSpec { enableMode() } } }
+ .holdLatestSpec(buildSpec { enableMode() })
+ modeChangeEvents =
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
+ activeMode.map { it.first }
+ }
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all state accumulation is immediately started. When the returned
+ * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
+ * accumulation in the process.
+ *
+ * Use [compiledStateful] to compile and stand-up a mode graph.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+fun interface StatefulMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+ * new mode.
+ */
+ fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
+}
+
+/**
+ * Returns a [Stateful] that, when [applied][StateScope.applyStateful], stands up a modal-transition
+ * graph starting with this [StatefulMode], automatically switching to new modes as they are
+ * produced.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
+ get() = statefully {
+ var modeChangeEvents by EventsLoop<StatefulMode<A>>()
+ val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { statefully { enableMode() } } }
+ .holdLatestStateful(statefully { enableMode() })
+ modeChangeEvents =
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
+ activeMode.map { it.first }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
new file mode 100644
index 000000000000..f7decbb66de8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.DerivedMapCheap
+import com.android.systemui.kairos.internal.StateImpl
+import com.android.systemui.kairos.internal.init
+
+/**
+ * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
+ * holding a specific value.
+ *
+ * An example:
+ * ```
+ * val intState: State<Int> = ...
+ * val intSelector: StateSelector<Int> = intState.selector()
+ * // Tracks if intState is holding 1
+ * val isOne: State<Boolean> = intSelector.whenSelected(1)
+ * ```
+ *
+ * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
+ * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [State.map] operations used to track a
+ * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
+ * [State] to update, and so operates in `O(1)`.
+ *
+ * Note that the returned [StateSelector] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
+ StateSelector(
+ this,
+ changes
+ .map { new -> mapOf(new to true, sampleDeferred().value to false) }
+ .groupByKey(numDistinctValues),
+ )
+
+/**
+ * Tracks the currently selected value of type [A] from an upstream [State].
+ *
+ * @see selector
+ */
+@ExperimentalKairosApi
+class StateSelector<in A>
+internal constructor(
+ private val upstream: State<A>,
+ private val groupedChanges: GroupedEvents<A, Boolean>,
+) {
+ /**
+ * Returns a [State] that tracks whether the upstream [State] is currently holding the given
+ * [value].
+ *
+ * @see selector
+ */
+ fun whenSelected(value: A): State<Boolean> {
+ val operatorName = "StateSelector#whenSelected"
+ val name = "$operatorName[$value]"
+ return StateInit(
+ init(name) {
+ StateImpl(
+ name,
+ operatorName,
+ groupedChanges.impl.eventsForKey(value),
+ DerivedMapCheap(upstream.init) { it == value },
+ )
+ }
+ )
+ }
+
+ operator fun get(value: A): State<Boolean> = whenSelected(value)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
index 1f0a19d5752b..22ca83c6a15a 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
@@ -17,7 +17,6 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DerivedMapCheap
import com.android.systemui.kairos.internal.EventsImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
@@ -37,20 +36,31 @@ import com.android.systemui.kairos.internal.mapImpl
import com.android.systemui.kairos.internal.mapStateImpl
import com.android.systemui.kairos.internal.mapStateImplCheap
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.zipStateMap
-import com.android.systemui.kairos.internal.zipStates
+import com.android.systemui.kairos.util.WithPrev
import kotlin.reflect.KProperty
/**
- * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
- * holds a value, and an [Events] that emits when the value changes.
+ * A time-varying value with discrete changes. Conceptually, a combination of a [Transactional] that
+ * holds a value, and an [Events] that emits when the value [changes].
+ *
+ * [States][State] follow these rules:
+ * 1. In the same transaction that [changes] emits a new value, [sample] will continue to return the
+ * previous value.
+ * 2. Unless it is [constant][stateOf], [States][State] can only be created via [StateScope]
+ * operations, or derived from other existing [States][State] via [State.map], [combine], etc.
+ * 3. [States][State] can only be [sampled][TransactionScope.sample] within a [TransactionScope].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.states
*/
@ExperimentalKairosApi
sealed class State<out A> {
internal abstract val init: Init<StateImpl<A>>
}
-/** A [State] that never changes. */
+/**
+ * Returns a constant [State] that never changes. [changes] is equivalent to [emptyEvents] and
+ * [TransactionScope.sample] will always produce [value].
+ */
@ExperimentalKairosApi
fun <A> stateOf(value: A): State<A> {
val operatorName = "stateOf"
@@ -65,6 +75,10 @@ fun <A> stateOf(value: A): State<A> {
* will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<State<A>>.defer() = deferredState { value }
+ * ```
*/
@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value }
@@ -76,6 +90,10 @@ fun <A> stateOf(value: A): State<A> {
* and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<State<A>>.defer() = deferredState { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value }
@@ -94,6 +112,8 @@ fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline
/**
* Returns a [State] containing the results of applying [transform] to the value held by the
* original [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapState
*/
@ExperimentalKairosApi
fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
@@ -110,10 +130,12 @@ fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
* Returns a [State] that transforms the value held inside this [State] by applying it to the
* [transform].
*
- * Note that unlike [map], the result is not cached. This means that not only should [transform] be
- * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for
- * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an
- * observable change to the returned [State].
+ * Note that unlike [State.map], the result is not cached. This means that not only should
+ * [transform] be fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that
+ * [changes] for the returned [State] will operate unexpectedly, emitting at rates that do not
+ * reflect an observable change to the returned [State].
+ *
+ * @see State.map
*/
@ExperimentalKairosApi
fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
@@ -125,214 +147,30 @@ fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
}
/**
- * Returns a [State] by combining the values held inside the given [State]s by applying them to the
- * given function [transform].
- */
-@ExperimentalKairosApi
-fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
- combine(this, other, transform)
-
-/**
* Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half
* of the original.
*
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ * fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
+ * val first = map { it.first }
+ * val second = map { it.second }
+ * return first to second
+ * }
* ```
*/
@ExperimentalKairosApi
fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
- val left = map { it.first }
- val right = map { it.second }
- return left to right
-}
-
-/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [List].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A> Iterable<State<A>>.combine(): State<List<A>> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- val states = map { it.init }
- zipStates(
- name,
- operatorName,
- states.size,
- states = init(null) { states.map { it.connect(this) } },
- )
- }
- )
-}
-
-/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStateMap(
- name,
- operatorName,
- size,
- states = init(null) { mapValues { it.value.init.connect(evalScope = this) } },
- )
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
- combine().map(transform)
-
-/**
- * Returns a [State] by combining the values held inside the given [State]s into a [List].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
- states.asIterable().combine(transform)
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- transform: KairosScope.(A, B) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
- NoScope.transform(a, b)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- transform: KairosScope.(A, B, C) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
- NoScope.transform(a, b, c)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, D, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- stateD: State<D>,
- transform: KairosScope.(A, B, C, D) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
- a,
- b,
- c,
- d ->
- NoScope.transform(a, b, c, d)
- }
- }
- )
+ val first = map { it.first }
+ val second = map { it.second }
+ return first to second
}
/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
+ * Returns a [State] by applying [transform] to the value held by the original [State].
*
- * @see State.combineWith
+ * @sample com.android.systemui.kairos.KairosSamples.flatMap
*/
@ExperimentalKairosApi
-fun <A, B, C, D, E, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- stateD: State<D>,
- stateE: State<E>,
- transform: KairosScope.(A, B, C, D, E) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(
- name,
- operatorName,
- stateA.init,
- stateB.init,
- stateC.init,
- stateD.init,
- stateE.init,
- ) { a, b, c, d, e ->
- NoScope.transform(a, b, c, d, e)
- }
- }
- )
-}
-
-/** Returns a [State] by applying [transform] to the value held by the original [State]. */
-@ExperimentalKairosApi
fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> {
val operatorName = "flatMap"
val name = operatorName
@@ -345,75 +183,16 @@ fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> {
)
}
-/** Shorthand for `flatMap { it }` */
-@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it }
-
/**
- * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
- * holding a specific value.
+ * Returns a [State] that behaves like the current value of the original [State].
*
- * An example:
- * ```
- * val intState: State<Int> = ...
- * val intSelector: StateSelector<Int> = intState.selector()
- * // Tracks if lInt is holding 1
- * val isOne: State<Boolean> = intSelector.whenSelected(1)
+ * ``` kotlin
+ * fun <A> State<State<A>>.flatten() = flatMap { it }
* ```
*
- * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
- * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [State.map] operations used to track a
- * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
- * [State] to update, and so operates in `O(1)`.
- *
- * Note that the returned [StateSelector] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see groupByKey
+ * @see flatMap
*/
-@ExperimentalKairosApi
-fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
- StateSelector(
- this,
- changes
- .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
- .groupByKey(numDistinctValues),
- )
-
-/**
- * Tracks the currently selected value of type [A] from an upstream [State].
- *
- * @see selector
- */
-@ExperimentalKairosApi
-class StateSelector<in A>
-internal constructor(
- private val upstream: State<A>,
- private val groupedChanges: GroupedEvents<A, Boolean>,
-) {
- /**
- * Returns a [State] that tracks whether the upstream [State] is currently holding the given
- * [value].
- *
- * @see selector
- */
- fun whenSelected(value: A): State<Boolean> {
- val operatorName = "StateSelector#whenSelected"
- val name = "$operatorName[$value]"
- return StateInit(
- init(name) {
- StateImpl(
- name,
- operatorName,
- groupedChanges.impl.eventsForKey(value),
- DerivedMapCheap(upstream.init) { it == value },
- )
- }
- )
- }
-
- operator fun get(value: A): State<Boolean> = whenSelected(value)
-}
+@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it }
/**
* A mutable [State] that provides the ability to manually [set its value][setValue].
@@ -441,6 +220,9 @@ class MutableState<T> internal constructor(internal val network: Network, initia
override val init: Init<StateImpl<T>>
get() = state.init
+ // TODO: not convinced this is totally safe
+ // - at least for the BuildScope smart-constructor, we can avoid the network.transaction { }
+ // call since we're already in a transaction
internal val state = run {
val changes = input.impl
val name = null
@@ -491,7 +273,17 @@ class MutableState<T> internal constructor(internal val network: Network, initia
fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped)
}
-/** A forward-reference to a [State], allowing for recursive definitions. */
+/**
+ * A forward-reference to a [State]. Useful for recursive definitions.
+ *
+ * This reference can be used like a standard [State], but will throw an error if its [loopback] is
+ * unset before it is [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ *
+ * Note that it is safe to invoke [TransactionScope.sampleDeferred] before [loopback] is set,
+ * provided the returned [DeferredValue] is not [queried][KairosScope.get].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.stateLoop
+ */
@ExperimentalKairosApi
class StateLoop<A> : State<A>() {
@@ -502,7 +294,10 @@ class StateLoop<A> : State<A>() {
override val init: Init<StateImpl<A>> =
init(name) { deferred.value.init.connect(evalScope = this) }
- /** The [State] this [StateLoop] will forward to. */
+ /**
+ * The [State] this reference is referring to. Must be set before this [StateLoop] is
+ * [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ */
var loopback: State<A>? = null
set(value) {
value?.let {
@@ -528,3 +323,24 @@ internal class StateInit<A> internal constructor(override val init: Init<StateIm
private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> =
StateInit(init(name = null) { block().init.connect(evalScope = this) })
+
+/**
+ * Like [changes] but also includes the old value of this [State].
+ *
+ * Shorthand for:
+ * ``` kotlin
+ * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
+ * ```
+ */
+@ExperimentalKairosApi
+val <A> State<A>.transitions: Events<WithPrev<A, A>>
+ get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
+
+/**
+ * Returns an [Events] that emits the new value of this [State] when it changes.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.changes
+ */
+@ExperimentalKairosApi
+val <A> State<A>.changes: Events<A>
+ get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
index 933ff1a75a02..faeffe84e2e8 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
@@ -16,13 +16,11 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.util.MapPatch
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.mapMaybeValues
-import com.android.systemui.kairos.util.none
import com.android.systemui.kairos.util.zipWith
// TODO: caching story? should each Scope have a cache of applied Stateful instances?
@@ -64,6 +62,8 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * @see holdState
*/
fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A>
@@ -71,113 +71,128 @@ interface StateScope : TransactionScope {
* Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
*
* The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
- * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
- * an associated value of [none] will remove the key from the tracked [Map].
+ * map, an associated value of [present][Maybe.present] will insert or replace the value in the
+ * tracked [Map], and an associated value of [absent][Maybe.absent] will remove the key from the
+ * tracked [Map].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ * @see MapPatch
*/
- fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
initialValues: DeferredValue<Map<K, V>>
): Incremental<K, V>
+ /**
+ * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
+ * original [Events].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
+ * of the original [Events].
+ */
+ fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
+
+ /**
+ * Returns an [Events] containing the results of applying each [Stateful] emitted from the
+ * original [Events], and a [DeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
+ * with the same key is stopped.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ */
+ fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
+ init: DeferredValue<Map<K, Stateful<B>>>,
+ numKeys: Int? = null,
+ ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>>
+
// TODO: everything below this comment can be made into extensions once we have context params
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- * initialEvents: Map<K, Events<V>>,
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ * initialEvents: DeferredValue<Map<K, Events<V>>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementally
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- name: String? = null,
- initialEvents: DeferredValue<Map<K, Events<V>>>,
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ initialEvents: DeferredValue<Map<K, Events<V>>>
): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementally()
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- * initialEvents: Map<K, Events<V>>,
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ * initialEvents: DeferredValue<Map<K, Events<V>>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementallyPromptly
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- initialEvents: DeferredValue<Map<K, Events<V>>>,
- name: String? = null,
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ initialEvents: DeferredValue<Map<K, Events<V>>>
): Events<Map<K, V>> =
foldStateMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly()
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
* initialEvents: Map<K, Events<V>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementally
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- name: String? = null,
- initialEvents: Map<K, Events<V>> = emptyMap(),
- ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents))
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ initialEvents: Map<K, Events<V>> = emptyMap()
+ ): Events<Map<K, V>> = mergeEventsIncrementally(deferredOf(initialEvents))
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
* initialEvents: Map<K, Events<V>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementallyPromptly
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- initialEvents: Map<K, Events<V>> = emptyMap(),
- name: String? = null,
- ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name)
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ initialEvents: Map<K, Events<V>> = emptyMap()
+ ): Events<Map<K, V>> = mergeEventsIncrementallyPromptly(deferredOf(initialEvents))
/** Applies the [Stateful] within this [StateScope]. */
fun <A> Stateful<A>.applyStateful(): A = this()
/**
- * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue].
+ * Applies the [Stateful] within this [StateScope], returning the result as a [DeferredValue].
*/
fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope {
applyStateful()
@@ -190,42 +205,59 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.holdState
+ * @see holdStateDeferred
*/
fun <A> Events<A>.holdState(initialValue: A): State<A> =
holdStateDeferred(deferredOf(initialValue))
/**
- * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
- * original [Events].
- *
- * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
- * of the original [Events].
- */
- fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
-
- /**
* Returns an [Events] containing the results of applying [transform] to each value of the
* original [Events].
*
* [transform] can perform state accumulation via its [StateScope] receiver. Unlike
* [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
* original [Events].
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
+ * map { statefully { transform(it) } }.applyStatefuls()
+ * ```
*/
fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
- map { statefully { transform(it) } }.applyStatefuls()
+ mapCheap { statefully { transform(it) } }.applyStatefuls()
/**
* Returns a [State] the holds the result of applying the [Stateful] held by the original
* [State].
*
* Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
+ *
+ * ``` kotlin
+ * fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
+ * changes
+ * .applyStatefuls()
+ * .holdState(initialValue = sample().applyStateful())
+ * ```
*/
fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
changes
.applyStatefuls()
- .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+ .holdStateDeferred(
+ initialValue = deferredStateScope { sampleDeferred().value.applyStateful() }
+ )
- /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */
+ /**
+ * Returns an [Events] that acts like the most recent [Events] to be emitted from the original
+ * [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
+ * ```
+ *
+ * @see switchEvents
+ */
fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
/**
@@ -234,9 +266,14 @@ interface StateScope : TransactionScope {
*
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
+ * map { statefully { transform(it) } }.applyLatestStateful()
+ * ```
*/
fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
- map { statefully { transform(it) } }.applyLatestStateful()
+ mapCheap { statefully { transform(it) } }.applyLatestStateful()
/**
* Returns an [Events] that switches to a new [Events] produced by [transform] every time the
@@ -244,6 +281,13 @@ interface StateScope : TransactionScope {
*
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.flatMapLatestStateful(
+ * transform: StateScope.(A) -> Events<B>
+ * ): Events<B> =
+ * mapLatestStateful(transform).flatten()
+ * ```
*/
fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> =
mapLatestStateful(transform).flatten()
@@ -254,6 +298,8 @@ interface StateScope : TransactionScope {
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
* stopped.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.applyLatestStateful
*/
fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first
@@ -281,19 +327,18 @@ interface StateScope : TransactionScope {
init: Stateful<A>
): Pair<Events<B>, DeferredValue<A>> {
val (events, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
+ mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
.applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
val outEvents: Events<B> =
events.mapMaybe {
checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
}
val outInit: DeferredValue<A> = deferredTransactionScope {
- val initResult: Map<Unit, A> = result.get()
+ val initResult: Map<Unit, A> = result.value
check(Unit in initResult) {
"applyLatest: expected initial result, but none present in: $initResult"
}
- @Suppress("UNCHECKED_CAST")
- initResult.getOrDefault(Unit) { null } as A
+ initResult.getValue(Unit)
}
return Pair(outEvents, outInit)
}
@@ -303,34 +348,32 @@ interface StateScope : TransactionScope {
* original [Events], and a [DeferredValue] containing the result of applying [init]
* immediately.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
- *
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
- init: DeferredValue<Map<K, Stateful<B>>>,
+ fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
+ init: Map<K, Stateful<B>>,
numKeys: Int? = null,
- ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
+ ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>> =
+ applyLatestStatefulForKey(deferredOf(init), numKeys)
/**
- * Returns an [Events] containing the results of applying each [Stateful] emitted from the
- * original [Events], and a [DeferredValue] containing the result of applying [init]
- * immediately.
+ * Returns an [Incremental] containing the latest results of applying each [Stateful] emitted
+ * from the original [Incremental]'s [updates].
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
- init: Map<K, Stateful<B>>,
- numKeys: Int? = null,
- ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
- applyLatestStatefulForKey(deferredOf(init), numKeys)
-
fun <K, V> Incremental<K, Stateful<V>>.applyLatestStatefulForKey(
numKeys: Int? = null
): Incremental<K, V> {
@@ -345,10 +388,12 @@ interface StateScope : TransactionScope {
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
init: DeferredValue<Map<K, Stateful<A>>>,
numKeys: Int? = null,
): Incremental<K, A> {
@@ -363,28 +408,33 @@ interface StateScope : TransactionScope {
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
init: Map<K, Stateful<A>> = emptyMap(),
numKeys: Int? = null,
): Incremental<K, A> = holdLatestStatefulForKey(deferredOf(init), numKeys)
/**
* Returns an [Events] containing the results of applying each [Stateful] emitted from the
- * original [Events], and a [DeferredValue] containing the result of applying [stateInit]
- * immediately.
+ * original [Events].
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.applyLatestStatefulForKey
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
numKeys: Int? = null
- ): Events<Map<K, Maybe<A>>> =
+ ): Events<MapPatch<K, A>> =
applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first
/**
@@ -395,18 +445,20 @@ interface StateScope : TransactionScope {
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
initialValues: DeferredValue<Map<K, A>>,
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
.applyLatestStatefulForKey(
deferredStateScope {
- initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+ initialValues.value.mapValues { (_, v) -> statefully { transform(v) } }
},
numKeys = numKeys,
)
@@ -419,14 +471,16 @@ interface StateScope : TransactionScope {
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
initialValues: Map<K, A>,
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
/**
@@ -436,13 +490,24 @@ interface StateScope : TransactionScope {
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ *
+ * ``` kotlin
+ * fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
+ * numKeys: Int? = null,
+ * transform: StateScope.(A) -> B,
+ * ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
+ * map { patch -> patch.mapValues { (_, mv) -> mv.map { statefully { transform(it) } } } }
+ * .applyLatestStatefulForKey(numKeys)
+ * ```
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+ ): Events<MapPatch<K, B>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
/**
* Returns an [Events] that will only emit the next event of the original [Events], and then
@@ -450,18 +515,31 @@ interface StateScope : TransactionScope {
*
* If the original [Events] is emitting an event at this exact time, then it will be the only
* even emitted from the result [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.nextOnly(): Events<A> =
+ * EventsLoop<A>().apply {
+ * loopback = map { emptyEvents }.holdState(this@nextOnly).switchEvents()
+ * }
+ * ```
*/
- fun <A> Events<A>.nextOnly(name: String? = null): Events<A> =
+ fun <A> Events<A>.nextOnly(): Events<A> =
if (this === emptyEvents) {
this
} else {
- EventsLoop<A>().also {
- it.loopback =
- it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name)
+ EventsLoop<A>().apply {
+ loopback = mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents()
}
}
- /** Returns an [Events] that skips the next emission of the original [Events]. */
+ /**
+ * Returns an [Events] that skips the next emission of the original [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.skipNext(): Events<A> =
+ * nextOnly().map { this@skipNext }.holdState(emptyEvents).switchEvents()
+ * ```
+ */
fun <A> Events<A>.skipNext(): Events<A> =
if (this === emptyEvents) {
this
@@ -475,6 +553,11 @@ interface StateScope : TransactionScope {
*
* If the original [Events] emits at the same time as [stop], then the returned [Events] will
* emit that value.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
+ * stop.map { emptyEvents }.nextOnly().holdState(this).switchEvents()
+ * ```
*/
fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
if (stop === emptyEvents) {
@@ -494,14 +577,19 @@ interface StateScope : TransactionScope {
val (_, init: DeferredValue<Map<Unit, A>>) =
stop
.nextOnly()
- .map { mapOf(Unit to none<Stateful<A>>()) }
+ .map { mapOf(Unit to Maybe.absent<Stateful<A>>()) }
.applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
- return deferredStateScope { init.get().getValue(Unit) }
+ return deferredStateScope { init.value.getValue(Unit) }
}
/**
* Returns an [Events] that emits values from the original [Events] up to and including a value
* is emitted that satisfies [predicate].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ * takeUntil(filter(predicate))
+ * ```
*/
fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
takeUntil(filter(predicate))
@@ -513,6 +601,18 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.foldState(
+ * initialValue: B,
+ * transform: TransactionScope.(A, B) -> B,
+ * ): State<B> {
+ * lateinit var state: State<B>
+ * return map { a -> transform(a, state.sample()) }
+ * .holdState(initialValue)
+ * .also { state = it }
+ * }
+ * ```
*/
fun <A, B> Events<A>.foldState(
initialValue: B,
@@ -529,6 +629,18 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.foldStateDeferred(
+ * initialValue: DeferredValue<B>,
+ * transform: TransactionScope.(A, B) -> B,
+ * ): State<B> {
+ * lateinit var state: State<B>
+ * return map { a -> transform(a, state.sample()) }
+ * .holdStateDeferred(initialValue)
+ * .also { state = it }
+ * }
+ * ```
*/
fun <A, B> Events<A>.foldStateDeferred(
initialValue: DeferredValue<B>,
@@ -551,10 +663,11 @@ interface StateScope : TransactionScope {
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
*
- * Shorthand for:
- * ```kotlin
- * val (changes, initApplied) = applyLatestStateful(init)
- * return changes.holdStateDeferred(initApplied)
+ * ``` kotlin
+ * fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
+ * val (changes, initApplied) = applyLatestStateful(init)
+ * return changes.holdStateDeferred(initApplied)
+ * }
* ```
*/
fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
@@ -578,8 +691,8 @@ interface StateScope : TransactionScope {
* that the returned [Events] will not emit until the original [Events] has emitted twice.
*/
fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> =
- mapCheap { just(it) }
- .pairwise(none)
+ mapCheap { Maybe.present(it) }
+ .pairwise(Maybe.absent)
.mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
/**
@@ -599,10 +712,11 @@ interface StateScope : TransactionScope {
* Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
*
* The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
- * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
- * an associated value of [none] will remove the key from the tracked [Map].
+ * map, an associated value of [Maybe.present] will insert or replace the value in the tracked
+ * [Map], and an associated value of [absent][Maybe.absent] will remove the key from the tracked
+ * [Map].
*/
- fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
initialValues: Map<K, V> = emptyMap()
): Incremental<K, V> = foldStateMapIncrementally(deferredOf(initialValues))
@@ -610,10 +724,11 @@ interface StateScope : TransactionScope {
* Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue],
* containing the emitted value and its index (starting from zero).
*
- * Shorthand for:
- * ```
- * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
- * sample(index) { a, idx -> IndexedValue(idx, a) }
+ * ``` kotlin
+ * fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
+ * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
+ * return sample(index) { a, idx -> IndexedValue(idx, a) }
+ * }
* ```
*/
fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
@@ -625,9 +740,11 @@ interface StateScope : TransactionScope {
* Returns an [Events] containing the results of applying [transform] to each value of the
* original [Events] and its index (starting from zero).
*
- * Shorthand for:
- * ```
- * withIndex().map { (idx, a) -> transform(idx, a) }
+ * ``` kotlin
+ * fun <A> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
+ * val index = foldState(0) { _, i -> i + 1 }
+ * return sample(index) { a, idx -> transform(idx, a) }
+ * }
* ```
*/
fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
@@ -635,7 +752,16 @@ interface StateScope : TransactionScope {
return sample(index) { a, idx -> transform(idx, a) }
}
- /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */
+ /**
+ * Returns an [Events] where all subsequent repetitions of the same value are filtered out.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.distinctUntilChanged(): Events<A> {
+ * val state: State<Any?> = holdState(Any())
+ * return filter { it != state.sample() }
+ * }
+ * ```
+ */
fun <A> Events<A>.distinctUntilChanged(): Events<A> {
val state: State<Any?> = holdState(Any())
return filter { it != state.sample() }
@@ -647,18 +773,35 @@ interface StateScope : TransactionScope {
*
* Note that the returned [Events] will not emit anything until [other] has emitted at least one
* value.
+ *
+ * ``` kotlin
+ * fun <A, B, C> Events<A>.sample(
+ * other: Events<B>,
+ * transform: TransactionScope.(A, B) -> C,
+ * ): Events<C> {
+ * val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+ * return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
+ * }
+ * ```
*/
fun <A, B, C> Events<A>.sample(
other: Events<B>,
transform: TransactionScope.(A, B) -> C,
): Events<C> {
- val state = other.mapCheap { just(it) }.holdState(none)
- return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
+ val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+ return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
}
/**
* Returns a [State] that samples the [Transactional] held by the given [State] within the same
* transaction that the state changes.
+ *
+ * ``` kotlin
+ * fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
+ * changes
+ * .sampleTransactionals()
+ * .holdStateDeferred(deferredTransactionScope { sample().sample() })
+ * ```
*/
fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
changes
@@ -668,6 +811,14 @@ interface StateScope : TransactionScope {
/**
* Returns a [State] that transforms the value held inside this [State] by applying it to the
* given function [transform].
+ *
+ * Note that this is less efficient than [State.map], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
+ * map { transactionally { transform(it) } }.sampleTransactionals()
+ * ```
*/
fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
map { transactionally { transform(it) } }.sampleTransactionals()
@@ -676,7 +827,20 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B, Z> combineTransactionally(
+ * stateA: State<A>,
+ * stateB: State<B>,
+ * transform: TransactionScope.(A, B) -> Z,
+ * ): State<Z> =
+ * combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
+ * .sampleTransactionals()
+ * ```
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, Z> combineTransactionally(
stateA: State<A>,
@@ -690,7 +854,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, C, Z> combineTransactionally(
stateA: State<A>,
@@ -705,7 +872,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, C, D, Z> combineTransactionally(
stateA: State<A>,
@@ -719,7 +889,18 @@ interface StateScope : TransactionScope {
}
.sampleTransactionals()
- /** Returns a [State] by applying [transform] to the value held by the original [State]. */
+ /**
+ * Returns a [State] by applying [transform] to the value held by the original [State].
+ *
+ * Note that this is less efficient than [flatMap], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B> State<A>.flatMapTransactionally(
+ * transform: TransactionScope.(A) -> State<B>
+ * ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
+ * ```
+ */
fun <A, B> State<A>.flatMapTransactionally(
transform: TransactionScope.(A) -> State<B>
): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
@@ -728,7 +909,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, Z> combineTransactionally(
vararg states: State<A>,
@@ -739,7 +923,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, Z> Iterable<State<A>>.combineTransactionally(
transform: TransactionScope.(List<A>) -> Z
@@ -748,8 +935,13 @@ interface StateScope : TransactionScope {
/**
* Returns a [State] by combining the values held inside the given [State]s by applying them to
* the given function [transform].
+ *
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
*/
- fun <A, B, C> State<A>.combineWithTransactionally(
+ @Suppress("INAPPLICABLE_JVM_NAME")
+ @JvmName(name = "combineStateTransactionally")
+ fun <A, B, C> State<A>.combineTransactionally(
other: State<B>,
transform: TransactionScope.(A, B) -> C,
): State<C> = combineTransactionally(this, other, transform)
@@ -757,6 +949,15 @@ interface StateScope : TransactionScope {
/**
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the state of the original's values.
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
+ * mapValues { (_, v) -> v.changes }
+ * .mergeEventsIncrementallyPromptly()
+ * .foldStateMapIncrementally(
+ * deferredStateScope { sample().mapMaybeValues { (_, s) -> s.sample() } }
+ * )
+ * ```
*/
fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
mapValues { (_, v) -> v.changes }
@@ -769,6 +970,12 @@ interface StateScope : TransactionScope {
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the [State] returned from applying [transform] to the original's
* entries.
+ *
+ * ``` kotlin
+ * fun <K, V, U> Incremental<K, V>.mapIncrementalState(
+ * transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
+ * ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally()
+ * ```
*/
fun <K, V, U> Incremental<K, V>.mapIncrementalState(
transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
@@ -778,16 +985,33 @@ interface StateScope : TransactionScope {
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the [State] returned from applying [transform] to the original's
* entries, such that entries are added when that state is `true`, and removed when `false`.
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, V>.filterIncrementally(
+ * transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
+ * ): Incremental<K, V> = mapIncrementalState { entry ->
+ * transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
+ * }
+ * ```
*/
fun <K, V> Incremental<K, V>.filterIncrementally(
transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
): Incremental<K, V> = mapIncrementalState { entry ->
- transform(entry).map { if (it) just(entry.value) else none }
+ transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
}
/**
* Returns an [Incremental] that samples the [Transactionals][Transactional] held by the
* original within the same transaction that the incremental [updates].
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
+ * updates
+ * .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } }
+ * .foldStateMapIncrementally(
+ * deferredStateScope { sample().mapValues { (k, v) -> v.sample() } }
+ * )
+ * ```
*/
fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
updates
@@ -799,6 +1023,16 @@ interface StateScope : TransactionScope {
/**
* Returns an [Incremental] that tracks the entries of the original incremental, but values
* replaced with those obtained by applying [transform] to each original entry.
+ *
+ * Note that this is less efficient than [mapValues], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
+ * transform: TransactionScope.(Map.Entry<K, V>) -> U
+ * ): Incremental<K, U> =
+ * mapValues { transactionally { transform(it) } }.sampleTransactionals()
+ * ```
*/
fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
transform: TransactionScope.(Map.Entry<K, V>) -> U
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
new file mode 100644
index 000000000000..63e27d05f73d
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.IncrementalImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.switchDeferredImplSingle
+import com.android.systemui.kairos.internal.switchPromptImplSingle
+import com.android.systemui.kairos.util.mapPatchFromFullDiff
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch does take effect until the *next* transaction after [State] changes. For a switch
+ * that takes effect immediately, see [switchEventsPromptly].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEvents
+ */
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEvents(): Events<A> {
+ val patches =
+ mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+ return EventsInit(
+ constInit(
+ name = null,
+ switchDeferredImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = { patches },
+ ),
+ )
+ )
+}
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch takes effect immediately within the same transaction that [State] changes. If the
+ * newly-switched-in [Events] is emitting a value within this transaction, then that value will be
+ * emitted from this switch. If not, but the previously-switched-in [Events] *is* emitting, then
+ * that value will be emitted from this switch instead. Otherwise, there will be no emission.
+ *
+ * In general, you should prefer [switchEvents] over this method. It is both safer and more
+ * performant.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEventsPromptly
+ */
+// TODO: parameter to handle coincidental emission from both old and new
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
+ val patches =
+ mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+ return EventsInit(
+ constInit(
+ name = null,
+ switchPromptImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = { patches },
+ ),
+ )
+ )
+}
+
+/** Returns an [Incremental] that behaves like current value of this [State]. */
+fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
+ val stateChangePatches =
+ transitions.mapNotNull { (old, new) ->
+ mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
+ }
+ val innerChanges =
+ map { inner ->
+ merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
+ switchPatch + upcomingPatch
+ }
+ }
+ .switchEventsPromptly()
+ val flattened = flatten()
+ return IncrementalInit(
+ init("switchIncremental") {
+ val upstream = flattened.init.connect(this)
+ IncrementalImpl(
+ "switchIncremental",
+ "switchIncremental",
+ upstream.changes,
+ innerChanges.init.connect(this),
+ upstream.store,
+ )
+ }
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
new file mode 100644
index 000000000000..3d2768ba9c4c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
+ * transactionally connect to / disconnect from the [Events] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
+ * transactionally connect to / disconnect from the [State] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("eventsSpecToColdConflatedFlow")
+fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("stateSpecToColdConflatedFlow")
+fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [Events].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalFlowToColdConflatedFlow")
+fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalStateToColdConflatedFlow")
+fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulFlowToColdConflatedFlow")
+fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulStateToColdConflatedFlow")
+fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
index 225416992d52..a5ac909b7a28 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
@@ -16,11 +16,14 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.These
+
/**
* Kairos operations that are available while a transaction is active.
*
* These operations do not accumulate state, which makes [TransactionScope] weaker than
- * [StateScope], but allows them to be used in more places.
+ * [StateScope], but allows it to be used in more places.
*/
@ExperimentalKairosApi
interface TransactionScope : KairosScope {
@@ -57,7 +60,7 @@ interface TransactionScope : KairosScope {
*/
fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A>
- /** An [Events] that emits once, within this transaction, and then never again. */
+ /** An [Events] that emits once, within the current transaction, and then never again. */
val now: Events<Unit>
/**
@@ -66,7 +69,7 @@ interface TransactionScope : KairosScope {
*
* @see sampleDeferred
*/
- fun <A> State<A>.sample(): A = sampleDeferred().get()
+ fun <A> State<A>.sample(): A = sampleDeferred().value
/**
* Returns the current value held by this [Transactional]. Guaranteed to be consistent within
@@ -74,5 +77,55 @@ interface TransactionScope : KairosScope {
*
* @see sampleDeferred
*/
- fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+ fun <A> Transactional<A>.sample(): A = sampleDeferred().value
}
+
+/**
+ * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
+ * emission of the original [Events], within the same transaction of the original emission.
+ */
+@ExperimentalKairosApi
+fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, state.sample()) }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+ sampleable: Transactional<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, sampleable.sample()) }
+
+/**
+ * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
+ * the new value is passed to [transform].
+ *
+ * Note that [sample] is both more performant and safer to use with recursive definitions. You will
+ * generally want to use it rather than this.
+ *
+ * @see sample
+ */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.samplePromptly(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> =
+ sample(state) { a, b -> These.first(a to b) }
+ .mergeWith(state.changes.map { These.second(it) }) { thiz, that ->
+ These.both((thiz as These.First).value, (that as These.Second).value)
+ }
+ .mapMaybe { these ->
+ when (these) {
+ // both present, transform the upstream value and the new value
+ is These.Both -> Maybe.present(transform(these.first.first, these.second))
+ // no upstream present, so don't perform the sample
+ is These.Second -> Maybe.absent()
+ // just the upstream, so transform the upstream and the old value
+ is These.First -> Maybe.present(transform(these.value.first, these.value.second))
+ }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
index 9485cd212603..cf98821fdadb 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
@@ -29,8 +29,8 @@ import com.android.systemui.kairos.internal.util.hashString
* it is "sampled", a new result may be produced.
*
* Because Kairos operates over an "idealized" model of Time that can be passed around as a data
- * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the
- * same (conceptual) time, in order to preserve _referential transparency_.
+ * type, [Transactionals][Transactional] are guaranteed to produce the same result if queried
+ * multiple times at the same (conceptual) time, in order to preserve _referential transparency_.
*/
@ExperimentalKairosApi
class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) {
@@ -50,6 +50,10 @@ fun <A> transactionalOf(value: A): Transactional<A> =
* queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Transactional<A>>.defer() = deferredTransactional { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value }
@@ -62,6 +66,10 @@ fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline
* [value][Lazy.value] will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Transactional<A>>.defer() = deferredTransactional { value }
+ * ```
*/
@ExperimentalKairosApi
fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
@@ -89,7 +97,13 @@ private inline fun <A> deferInline(
/**
* Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per
* transaction; any subsequent sampling within the same transaction will receive a cached value.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.sampleTransactional
*/
@ExperimentalKairosApi
fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> =
Transactional(stateOf(transactionalImpl { block() }))
+
+/** Returns a [Transactional] that, when queried, samples this [State]. */
+fun <A> State<A>.asTransactional(): Transactional<A> =
+ Transactional(map { TransactionalImpl.Const(CompletableLazy(it)) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
index b20e77a31dab..2f4c3963d2fe 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
@@ -26,6 +26,7 @@ import com.android.systemui.kairos.EventProducerScope
import com.android.systemui.kairos.Events
import com.android.systemui.kairos.EventsInit
import com.android.systemui.kairos.GroupedEvents
+import com.android.systemui.kairos.KairosCoroutineScope
import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.kairos.LocalNetwork
import com.android.systemui.kairos.MutableEvents
@@ -33,20 +34,23 @@ import com.android.systemui.kairos.TransactionScope
import com.android.systemui.kairos.groupByKey
import com.android.systemui.kairos.init
import com.android.systemui.kairos.internal.util.childScope
+import com.android.systemui.kairos.internal.util.invokeOnCancel
import com.android.systemui.kairos.internal.util.launchImmediate
import com.android.systemui.kairos.launchEffect
import com.android.systemui.kairos.mergeLeft
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import com.android.systemui.kairos.util.map
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.job
@@ -60,12 +64,8 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
LocalNetwork(network, coroutineScope, endSignal)
}
- override fun <T> events(
- name: String?,
- builder: suspend EventProducerScope<T>.() -> Unit,
- ): Events<T> =
+ override fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T> =
buildEvents(
- name,
constructEvents = { inputNode ->
val events = MutableEvents(network, inputNode)
events to
@@ -123,9 +123,9 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
val childScope = coroutineScope.childScope()
lateinit var cancelHandle: DisposableHandle
val handle = DisposableHandle {
- subRef.getAndSet(None)?.let { output ->
- cancelHandle.dispose()
- if (output is Just) {
+ cancelHandle.dispose()
+ subRef.getAndSet(Absent)?.let { output ->
+ if (output is Present) {
@Suppress("DeferredResultUnused")
network.transaction("observeEffect cancelled") {
scheduleDeactivation(output.value)
@@ -139,14 +139,27 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
val outputNode =
Output<A>(
context = coroutineContext,
- onDeath = { subRef.set(None) },
+ onDeath = { subRef.set(Absent) },
onEmit = { output ->
- if (subRef.get() is Just) {
+ if (subRef.get() is Present) {
// Not cancelled, safe to emit
val scope =
object : EffectScope, TransactionScope by this {
- override val effectCoroutineScope: CoroutineScope = childScope
- override val kairosNetwork: KairosNetwork = localNetwork
+ override fun <R> async(
+ context: CoroutineContext,
+ start: CoroutineStart,
+ block: suspend KairosCoroutineScope.() -> R,
+ ): Deferred<R> =
+ childScope.async(context, start) {
+ object : KairosCoroutineScope, CoroutineScope by this {
+ override val kairosNetwork: KairosNetwork
+ get() = localNetwork
+ }
+ .block()
+ }
+
+ override val kairosNetwork: KairosNetwork
+ get() = localNetwork
}
scope.block(output)
}
@@ -162,7 +175,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
.activate(evalScope = stateScope.evalScope, outputNode.schedulable)
?.let { (conn, needsEval) ->
outputNode.upstream = conn
- if (!subRef.compareAndSet(null, just(outputNode))) {
+ if (!subRef.compareAndSet(null, Maybe.present(outputNode))) {
// Job's already been cancelled, schedule deactivation
scheduleDeactivation(outputNode)
} else if (needsEval) {
@@ -289,21 +302,15 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
}
private fun mutableChildBuildScope(): BuildScopeImpl {
- val stopEmitter = newStopEmitter("mutableChildBuildScope")
val childScope = coroutineScope.childScope()
- childScope.coroutineContext.job.invokeOnCompletion { stopEmitter.emit(Unit) }
- // Ensure that once this transaction is done, the new child scope enters the completing
- // state (kept alive so long as there are child jobs).
- // TODO: need to keep the scope alive if it's used to accumulate state.
- // Otherwise, stopEmitter will emit early, due to the call to complete().
- // scheduleOutput(
- // OneShot {
- // // TODO: don't like this cast
- // (childScope.coroutineContext.job as CompletableJob).complete()
- // }
- // )
+ val stopEmitter = lazy {
+ newStopEmitter("mutableChildBuildScope").apply {
+ childScope.invokeOnCancel { emit(Unit) }
+ }
+ }
return BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter),
+ stateScope =
+ StateScopeImpl(evalScope = stateScope.evalScope, endSignalLazy = stopEmitter),
coroutineScope = childScope,
)
}
@@ -314,6 +321,7 @@ private fun EvalScope.reenterBuildScope(
coroutineScope: CoroutineScope,
) =
BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal),
+ stateScope =
+ StateScopeImpl(evalScope = this, endSignalLazy = outerScope.stateScope.endSignalLazy),
coroutineScope,
)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
index 9496b06c6bd1..f86e7612655f 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
@@ -19,16 +19,14 @@ package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.store.Single
import com.android.systemui.kairos.internal.store.SingletonMapK
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
-internal inline fun <A> filterJustImpl(
+internal inline fun <A> filterPresentImpl(
crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>>
): EventsImpl<A> =
DemuxImpl(
mapImpl(getPulse) { maybeResult, _ ->
- if (maybeResult is Just) {
+ if (maybeResult is Present) {
Single(maybeResult.value)
} else {
Single<A>()
@@ -43,6 +41,7 @@ internal inline fun <A> filterImpl(
crossinline getPulse: EvalScope.() -> EventsImpl<A>,
crossinline f: EvalScope.(A) -> Boolean,
): EventsImpl<A> {
- val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached()
- return filterJustImpl { mapped }
+ val mapped =
+ mapImpl(getPulse) { it, _ -> if (f(it)) Maybe.present(it) else Maybe.absent }.cached()
+ return filterPresentImpl { mapped }
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
index 8a3e01af6565..9b4778ab18b1 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
@@ -17,11 +17,10 @@
package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.store.StoreEntry
+import com.android.systemui.kairos.util.MapPatch
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.applyPatch
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.toMaybe
internal class IncrementalImpl<K, out V>(
name: String?,
@@ -48,12 +47,11 @@ internal inline fun <K, V> activatedIncremental(
val store = StateSource(init)
val maybeChanges =
mapImpl(getPatches) { patch, _ ->
- val (old, _) = store.getCurrentWithEpoch(evalScope = this)
- val new = old.applyPatch(patch)
- if (new != old) just(patch to new) else none
+ val (current, _) = store.getCurrentWithEpoch(evalScope = this)
+ current.applyPatchCalm(patch).toMaybe()
}
.cached()
- val calm = filterJustImpl { maybeChanges }
+ val calm = filterPresentImpl { maybeChanges }
val changes = mapImpl({ calm }) { (_, change), _ -> change }
val patches = mapImpl({ calm }) { (patch, _), _ -> patch }
evalScope.scheduleOutput(
@@ -70,22 +68,44 @@ internal inline fun <K, V> activatedIncremental(
return IncrementalImpl(name, operatorName, changes, patches, store)
}
+private fun <K, V> Map<K, V>.applyPatchCalm(
+ patch: MapPatch<K, V>
+): Pair<MapPatch<K, V>, Map<K, V>>? {
+ val current = this
+ val filteredPatch = mutableMapOf<K, Maybe<V>>()
+ val new = current.toMutableMap()
+ for ((key, change) in patch) {
+ when (change) {
+ is Maybe.Present -> {
+ if (key !in current || current.getValue(key) != change.value) {
+ filteredPatch[key] = change
+ new[key] = change.value
+ }
+ }
+ Maybe.Absent -> {
+ if (key in current) {
+ filteredPatch[key] = change
+ new.remove(key)
+ }
+ }
+ }
+ }
+ return if (filteredPatch.isNotEmpty()) filteredPatch to new else null
+}
+
internal inline fun <K, V> EventsImpl<Map<K, Maybe<V>>>.calmUpdates(
state: StateDerived<Map<K, V>>
): Pair<EventsImpl<Map<K, Maybe<V>>>, EventsImpl<Map<K, V>>> {
val maybeUpdate =
mapImpl({ this@calmUpdates }) { patch, _ ->
val (current, _) = state.getCurrentWithEpoch(evalScope = this)
- val new = current.applyPatch(patch)
- if (new != current) {
- state.setCacheFromPush(new, epoch)
- just(patch to new)
- } else {
- none
- }
+ current
+ .applyPatchCalm(patch)
+ ?.also { (_, newMap) -> state.setCacheFromPush(newMap, epoch) }
+ .toMaybe()
}
.cached()
- val calm = filterJustImpl { maybeUpdate }
+ val calm = filterPresentImpl { maybeUpdate }
val patches = mapImpl({ calm }) { (p, _), _ -> p }
val changes = mapImpl({ calm }) { (_, s), _ -> s }
return patches to changes
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
index 640c561a21eb..4fa107058f6e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
@@ -17,8 +17,6 @@
package com.android.systemui.kairos.internal
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
import kotlinx.coroutines.ExperimentalCoroutinesApi
/** Performs actions once, when the reactive component is first connected to the network. */
@@ -44,9 +42,9 @@ internal class Init<out A>(val name: String?, private val block: InitScope.() ->
@OptIn(ExperimentalCoroutinesApi::class)
fun getUnsafe(): Maybe<A> =
if (cache.isInitialized()) {
- just(cache.value.second)
+ Maybe.present(cache.value.second)
} else {
- none
+ Maybe.absent
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
index cf74f755c98b..c11eb122597d 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
@@ -28,14 +28,13 @@ import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import com.android.systemui.kairos.util.These
import com.android.systemui.kairos.util.flatMap
import com.android.systemui.kairos.util.getMaybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.maybeThat
-import com.android.systemui.kairos.util.maybeThis
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
import com.android.systemui.kairos.util.merge
import com.android.systemui.kairos.util.orError
import com.android.systemui.kairos.util.these
@@ -133,8 +132,8 @@ internal class MuxDeferredNode<W, K, V>(
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
+ is Present -> adds.add(k to newUpstream.value)
+ Absent -> removes.add(k)
}
}
@@ -282,7 +281,8 @@ internal inline fun <A> switchDeferredImplSingle(
crossinline getStorage: EvalScope.() -> EventsImpl<A>,
crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
): EventsImpl<A> {
- val patches = mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+ val patches =
+ mapImpl(getPatches) { newEvents, _ -> singleOf(Maybe.present(newEvents)).asIterable() }
val switchDeferredImpl =
switchDeferredImpl(
name = name,
@@ -402,8 +402,8 @@ internal inline fun <A, B> mergeNodes(
): EventsImpl<These<A, B>> {
val storage =
listOf(
- mapImpl(getPulse) { it, _ -> These.thiz(it) },
- mapImpl(getOther) { it, _ -> These.that(it) },
+ mapImpl(getPulse) { it, _ -> These.first(it) },
+ mapImpl(getOther) { it, _ -> These.second(it) },
)
.asIterableWithIndex()
val switchNode =
@@ -417,9 +417,9 @@ internal inline fun <A, B> mergeNodes(
mapImpl({ switchNode }) { it, logIndent ->
val mergeResults = it.asArrayHolder()
val first =
- mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() }
+ mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeFirst() }
val second =
- mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() }
+ mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeSecond() }
these(first, second).orError { "unexpected missing merge result" }
}
return merged.cached()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
index 32aef5c7041b..cb2c6e51df66 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
@@ -24,9 +24,8 @@ import com.android.systemui.kairos.internal.util.LogIndent
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
internal class MuxPromptNode<W, K, V>(
val name: String?,
@@ -94,8 +93,8 @@ internal class MuxPromptNode<W, K, V>(
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
+ is Present -> adds.add(k to newUpstream.value)
+ Absent -> removes.add(k)
}
}
@@ -311,7 +310,9 @@ internal inline fun <A> switchPromptImplSingle(
switchPromptImpl(
getStorage = { singleOf(getStorage()).asIterable() },
getPatches = {
- mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+ mapImpl(getPatches) { newEvents, _ ->
+ singleOf(Maybe.present(newEvents)).asIterable()
+ }
},
storeFactory = SingletonMapK.Factory(),
)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
index fbc2b3644701..6e86dd150126 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
@@ -21,9 +21,7 @@ import com.android.systemui.kairos.internal.util.HeteroMap
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.ContinuationInterceptor
import kotlin.time.measureTime
@@ -33,6 +31,7 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@@ -148,6 +147,10 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
/** Evaluates [block] inside of a new transaction when the network is ready. */
fun <R> transaction(reason: String, block: suspend EvalScope.() -> R): Deferred<R> =
CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult ->
+ if (!coroutineScope.isActive) {
+ onResult.cancel()
+ return@also
+ }
val job =
coroutineScope.launch {
inputScheduleChan.send(
@@ -261,25 +264,25 @@ internal class ScheduledAction<T>(
private val onResult: CompletableDeferred<T>? = null,
private val onStartTransaction: suspend EvalScope.() -> T,
) {
- private var result: Maybe<T> = none
+ private var result: Maybe<T> = Maybe.absent
suspend fun started(evalScope: EvalScope) {
- result = just(onStartTransaction(evalScope))
+ result = Maybe.present(onStartTransaction(evalScope))
}
fun fail(ex: Exception) {
- result = none
+ result = Maybe.absent
onResult?.completeExceptionally(ex)
}
fun completed() {
if (onResult != null) {
when (val result = result) {
- is Just -> onResult.complete(result.value)
+ is Present -> onResult.complete(result.value)
else -> {}
}
}
- result = none
+ result = Maybe.absent
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
index 46127cb2276b..da832580e7d9 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
@@ -22,8 +22,6 @@ import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.store.StoreEntry
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
internal open class StateImpl<out A>(
val name: String?,
@@ -73,7 +71,7 @@ internal sealed class StateDerived<A> : StateStore<A>() {
fun getCachedUnsafe(): Maybe<A> {
@Suppress("UNCHECKED_CAST")
- return if (cache == EmptyCache) none else just(cache as A)
+ return if (cache == EmptyCache) Maybe.absent else Maybe.present(cache as A)
}
protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>?
@@ -117,7 +115,8 @@ internal class StateSource<S>(init: Lazy<S>) : StateStore<S>() {
override fun toString(): String = "StateImpl(current=$_current, writeEpoch=$writeEpoch)"
- fun getStorageUnsafe(): Maybe<S> = if (_current.isInitialized()) just(_current.value) else none
+ fun getStorageUnsafe(): Maybe<S> =
+ if (_current.isInitialized()) Maybe.present(_current.value) else Maybe.absent
}
internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
index bd1f94fca22f..53a704a25f13 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
@@ -36,10 +36,14 @@ import com.android.systemui.kairos.switchEvents
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.map
-internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) :
+internal class StateScopeImpl(val evalScope: EvalScope, val endSignalLazy: Lazy<Events<Any>>) :
InternalStateScope, EvalScope by evalScope {
- override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+ override val endSignal: Events<Any> by endSignalLazy
+
+ override val endSignalOnce: Events<Any> by lazy {
+ endSignal.nextOnlyInternal("StateScope.endSignal")
+ }
override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> =
DeferredValue(deferAsync { block() })
@@ -119,7 +123,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
}
override fun childStateScope(newEnd: Events<Any>) =
- StateScopeImpl(evalScope, merge(newEnd, endSignal))
+ StateScopeImpl(evalScope, lazy { merge(newEnd, endSignal) })
private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> =
if (endSignalOnce === emptyEvents) {
@@ -165,4 +169,4 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
}
private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
- StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal)
+ StateScopeImpl(evalScope = this, endSignalLazy = outerScope.endSignalLazy)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
index 9b6940d03270..c34e67ef6926 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
@@ -17,8 +17,7 @@
package com.android.systemui.kairos.internal.util
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
import java.util.concurrent.ConcurrentHashMap
private object NULL
@@ -32,7 +31,7 @@ internal class HeteroMap private constructor(private val store: ConcurrentHashMa
@Suppress("UNCHECKED_CAST")
operator fun <A> get(key: Key<A>): Maybe<A> =
- store[key]?.let { just((if (it === NULL) null else it) as A) } ?: None
+ store[key]?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
operator fun <A> set(key: Key<A>, value: A) {
store[key] = value ?: NULL
@@ -57,7 +56,7 @@ internal class HeteroMap private constructor(private val store: ConcurrentHashMa
@Suppress("UNCHECKED_CAST")
fun <A> remove(key: Key<A>): Maybe<A> =
- store.remove(key)?.let { just((if (it === NULL) null else it) as A) } ?: None
+ store.remove(key)?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
@Suppress("UNCHECKED_CAST")
fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
index 466a9f83b91f..d2a169ccc29c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
@@ -112,7 +112,7 @@ internal suspend fun awaitCancellationAndThen(block: suspend () -> Unit) {
}
}
-internal fun CoroutineScope.launchOnCancel(
+internal fun CoroutineScope.invokeOnCancel(
context: CoroutineContext = EmptyCoroutineContext,
block: () -> Unit,
): Job =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
index 957d46ff1ecd..9f17d5646577 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
@@ -18,100 +18,118 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
/**
- * Contains a value of two possibilities: `Left<A>` or `Right<B>`
+ * Contains a value of two possibilities: `First<A>` or `Second<B>`
*
* [Either] generalizes sealed classes the same way that [Pair] generalizes data classes; if a
* [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous
* set of two options.
*/
sealed interface Either<out A, out B> {
- /** An [Either] that contains a [Left] value. */
- @JvmInline value class Left<out A>(val value: A) : Either<A, Nothing>
+ /** An [Either] that contains a [First] value. */
+ @JvmInline value class First<out A>(val value: A) : Either<A, Nothing>
- /** An [Either] that contains a [Right] value. */
- @JvmInline value class Right<out B>(val value: B) : Either<Nothing, B>
+ /** An [Either] that contains a [Second] value. */
+ @JvmInline value class Second<out B>(val value: B) : Either<Nothing, B>
+
+ companion object {
+ /** Constructs an [Either] containing the first possibility. */
+ fun <A> first(value: A): Either<A, Nothing> = First(value)
+
+ /** Constructs a [Either] containing the second possibility. */
+ fun <B> second(value: B): Either<Nothing, B> = Second(value)
+ }
}
/**
- * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the
- * [Right] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [First] value, or the
+ * [Second] value unchanged.
*/
-inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> =
+inline fun <A, B, C> Either<A, C>.mapFirst(transform: (A) -> B): Either<B, C> =
when (this) {
- is Left -> Left(transform(value))
- is Right -> this
+ is First -> First(transform(value))
+ is Second -> this
}
/**
- * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the
- * [Left] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [Second] value, or the
+ * [First] value unchanged.
*/
-inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> =
+inline fun <A, B, C> Either<A, B>.mapSecond(transform: (B) -> C): Either<A, C> =
when (this) {
- is Left -> this
- is Right -> Right(transform(value))
+ is First -> this
+ is Second -> Second(transform(value))
}
-/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */
-inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> =
+/** Returns a [Maybe] containing the [First] value held by this [Either], if present. */
+inline fun <A> Either<A, *>.firstMaybe(): Maybe<A> =
when (this) {
- is Left -> just(value)
- else -> none
+ is First -> Maybe.present(value)
+ else -> Maybe.absent
}
-/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */
-inline fun <A> Either<A, *>.leftOrNull(): A? =
+/** Returns the [First] value held by this [Either], or `null` if this is a [Second] value. */
+inline fun <A> Either<A, *>.firstOrNull(): A? =
when (this) {
- is Left -> value
+ is First -> value
else -> null
}
-/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */
-inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> =
+/** Returns a [Maybe] containing the [Second] value held by this [Either], if present. */
+inline fun <B> Either<*, B>.secondMaybe(): Maybe<B> =
when (this) {
- is Right -> just(value)
- else -> none
+ is Second -> Maybe.present(value)
+ else -> Maybe.absent
}
-/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */
-inline fun <B> Either<*, B>.rightOrNull(): B? =
+/** Returns the [Second] value held by this [Either], or `null` if this is a [First] value. */
+inline fun <B> Either<*, B>.secondOrNull(): B? =
when (this) {
- is Right -> value
+ is Second -> value
else -> null
}
/**
- * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and
- * [Pair.second] contains all [Right] values.
+ * Returns a [These] containing either the [First] value as [These.first], or the [Second] value as
+ * [These.second]. Will never return a [These.both].
+ */
+fun <A, B> Either<A, B>.asThese(): These<A, B> =
+ when (this) {
+ is Second -> These.second(value)
+ is First -> These.first(value)
+ }
+
+/**
+ * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
*/
fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> {
- val lefts = mutableListOf<A>()
- val rights = mutableListOf<B>()
+ val firsts = mutableListOf<A>()
+ val seconds = mutableListOf<B>()
for (either in this) {
when (either) {
- is Left -> lefts.add(either.value)
- is Right -> rights.add(either.value)
+ is First -> firsts.add(either.value)
+ is Second -> seconds.add(either.value)
}
}
- return lefts to rights
+ return firsts to seconds
}
/**
- * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values,
- * and [Pair.second] contains all [Right] values.
+ * Partitions this map of [Either] values into two maps; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
*/
fun <K, A, B> Map<K, Either<A, B>>.partitionEithers(): Pair<Map<K, A>, Map<K, B>> {
- val lefts = mutableMapOf<K, A>()
- val rights = mutableMapOf<K, B>()
+ val firsts = mutableMapOf<K, A>()
+ val seconds = mutableMapOf<K, B>()
for ((k, e) in this) {
when (e) {
- is Left -> lefts[k] = e.value
- is Right -> rights[k] = e.value
+ is First -> firsts[k] = e.value
+ is Second -> seconds[k] = e.value
}
}
- return lefts to rights
+ return firsts to seconds
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
index f368cbf8f124..8fe41bc20dfa 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
@@ -16,9 +16,9 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
+import com.android.systemui.kairos.util.Maybe.Present
/** A "patch" that can be used to batch-update a [Map], via [applyPatch]. */
typealias MapPatch<K, V> = Map<K, Maybe<V>>
@@ -27,16 +27,16 @@ typealias MapPatch<K, V> = Map<K, Maybe<V>>
* Returns a new [Map] that has [patch] applied to the original map.
*
* For each entry in [patch]:
- * * a [Just] value will be included in the new map, replacing the entry in the original map with
+ * * a [Present] value will be included in the new map, replacing the entry in the original map with
* the same key, if present.
- * * a [Maybe.None] value will be omitted from the new map, excluding the entry in the original map
- * with the same key, if present.
+ * * a [Maybe.Absent] value will be omitted from the new map, excluding the entry in the original
+ * map with the same key, if present.
*/
fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> {
val (adds: List<Pair<K, V>>, removes: List<K>) =
patch
.asSequence()
- .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) }
+ .map { (k, v) -> if (v is Present) First(k to v.value) else Second(k) }
.partitionEithers()
val removed: Map<K, V> = this - removes.toSet()
val updated: Map<K, V> = removed + adds
@@ -47,11 +47,11 @@ fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> {
* Returns a [MapPatch] that, when applied, includes all of the values from the original [Map].
*
* Shorthand for:
- * ```kotlin
- * mapValues { just(it.value) }
+ * ``` kotlin
+ * mapValues { (key, value) -> Maybe.present(value) }
* ```
*/
-fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { just(it.value) }
+fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { Maybe.present(it.value) }
/**
* Returns a [MapPatch] that, when applied, includes all of the entries from [new] whose keys are
@@ -67,10 +67,10 @@ fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
val adds = new - old.keys
return buildMap {
for (removed in removes) {
- put(removed, none)
+ put(removed, Maybe.absent)
}
for ((newKey, newValue) in adds) {
- put(newKey, just(newValue))
+ put(newKey, Maybe.present(newValue))
}
}
}
@@ -86,13 +86,16 @@ fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
*/
fun <K, V> mapPatchFromFullDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
val removes = old.keys - new.keys
- val adds = new.mapMaybeValues { (k, v) -> if (k in old && v == old[k]) none else just(v) }
+ val adds =
+ new.mapMaybeValues { (k, v) ->
+ if (k in old && v == old[k]) Maybe.absent else Maybe.present(v)
+ }
return hashMapOf<K, Maybe<V>>().apply {
for (removed in removes) {
- put(removed, none)
+ put(removed, Maybe.absent)
}
for ((newKey, newValue) in adds) {
- put(newKey, just(newValue))
+ put(newKey, Maybe.present(newValue))
}
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
index 681218399d93..4754bc443329 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
@@ -18,8 +18,8 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -31,17 +31,28 @@ import kotlin.coroutines.suspendCoroutine
/** Represents a value that may or may not be present. */
sealed interface Maybe<out A> {
/** A [Maybe] value that is present. */
- @JvmInline value class Just<out A> internal constructor(val value: A) : Maybe<A>
+ @JvmInline value class Present<out A> internal constructor(val value: A) : Maybe<A>
/** A [Maybe] value that is not present. */
- data object None : Maybe<Nothing>
+ data object Absent : Maybe<Nothing>
+
+ companion object {
+ /** Returns a [Maybe] containing [value]. */
+ fun <A> present(value: A): Maybe<A> = Present(value)
+
+ /** A [Maybe] that is not present. */
+ val absent: Maybe<Nothing> = Absent
+
+ /** A [Maybe] that is not present. */
+ inline fun <A> absent(): Maybe<A> = Absent
+ }
}
/** Utilities to query [Maybe] instances from within a [maybe] block. */
@RestrictsSuspension
object MaybeScope {
suspend operator fun <A> Maybe<A>.not(): A = suspendCoroutine { k ->
- if (this is Just) k.resume(value)
+ if (this is Present) k.resume(value)
}
suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k ->
@@ -53,7 +64,8 @@ object MaybeScope {
* Returns a [Maybe] value produced by evaluating [block].
*
* [block] can use its [MaybeScope] receiver to query other [Maybe] values, automatically cancelling
- * execution of [block] and producing [None] when attempting to query a [Maybe] that is not present.
+ * execution of [block] and producing [Absent] when attempting to query a [Maybe] that is not
+ * present.
*
* This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing
* with complex combinations of nullables:
@@ -68,33 +80,30 @@ object MaybeScope {
* ```
*/
fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> {
- var maybeResult: Maybe<A> = None
+ var maybeResult: Maybe<A> = Absent
val k =
object : Continuation<A> {
override val context: CoroutineContext = EmptyCoroutineContext
override fun resumeWith(result: Result<A>) {
- maybeResult = result.getOrNull()?.let { just(it) } ?: None
+ maybeResult = result.getOrNull()?.let { Maybe.present(it) } ?: Absent
}
}
block.startCoroutine(MaybeScope, k)
return maybeResult
}
-/** Returns a [Just] containing this value, or [None] if `null`. */
+/** Returns a [Maybe] containing this value if it is not `null`. */
inline fun <A> (A?).toMaybe(): Maybe<A> = maybe(this)
-/** Returns a [Just] containing a non-null [value], or [None] if `null`. */
-inline fun <A> maybe(value: A?): Maybe<A> = value?.let(::just) ?: None
+/** Returns a [Maybe] containing [value] if it is not `null`. */
+inline fun <A> maybe(value: A?): Maybe<A> = value?.let { Maybe.present(it) } ?: Absent
-/** Returns a [Just] containing [value]. */
-fun <A> just(value: A): Maybe<A> = Just(value)
+/** Returns a [Maybe] that is absent. */
+fun <A> maybeOf(): Maybe<A> = Absent
-/** A [Maybe] that is not present. */
-val none: Maybe<Nothing> = None
-
-/** A [Maybe] that is not present. */
-inline fun <A> none(): Maybe<A> = None
+/** Returns a [Maybe] containing [value]. */
+fun <A> maybeOf(value: A): Maybe<A> = Present(value)
/** Returns the value present in this [Maybe], or `null` if not present. */
inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
@@ -105,22 +114,22 @@ inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
*/
inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> =
when (this) {
- is Just -> just(transform(value))
- is None -> None
+ is Present -> Maybe.present(transform(value))
+ is Absent -> Absent
}
/** Returns the result of applying [transform] to the value in the original [Maybe]. */
inline fun <A, B> Maybe<A>.flatMap(transform: (A) -> Maybe<B>): Maybe<B> =
when (this) {
- is Just -> transform(value)
- is None -> None
+ is Present -> transform(value)
+ is Absent -> Absent
}
/** Returns the value present in this [Maybe], or the result of [defaultValue] if not present. */
inline fun <A> Maybe<A>.orElseGet(defaultValue: () -> A): A =
when (this) {
- is Just -> value
- is None -> defaultValue()
+ is Present -> value
+ is Absent -> defaultValue()
}
/**
@@ -132,8 +141,8 @@ inline fun <A> Maybe<A>.orError(getMessage: () -> Any): A = orElseGet { error(ge
/** Returns the value present in this [Maybe], or [defaultValue] if not present. */
inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
when (this) {
- is Just -> value
- is None -> defaultValue
+ is Present -> value
+ is Absent -> defaultValue
}
/**
@@ -142,15 +151,16 @@ inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
*/
inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> =
when (this) {
- is Just -> if (predicate(value)) this else None
+ is Present -> if (predicate(value)) this else Absent
else -> this
}
/** Returns a [List] containing all values that are present in this [Iterable]. */
-fun <A> Iterable<Maybe<A>>.filterJust(): List<A> = asSequence().filterJust().toList()
+fun <A> Iterable<Maybe<A>>.filterPresent(): List<A> = asSequence().filterPresent().toList()
/** Returns a [List] containing all values that are present in this [Sequence]. */
-fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>().map { it.value }
+fun <A> Sequence<Maybe<A>>.filterPresent(): Sequence<A> =
+ filterIsInstance<Present<A>>().map { it.value }
// Align
@@ -160,23 +170,25 @@ fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>
*/
inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> =
when (this) {
- is Just -> {
+ is Present -> {
val a = value
when (other) {
- is Just -> {
+ is Present -> {
val b = other.value
- just(transform(These.both(a, b)))
+ Maybe.present(transform(These.both(a, b)))
}
- None -> just(transform(These.thiz(a)))
+
+ Absent -> Maybe.present(transform(These.first(a)))
}
}
- None ->
+ Absent ->
when (other) {
- is Just -> {
+ is Present -> {
val b = other.value
- just(transform(These.that(b)))
+ Maybe.present(transform(These.second(b)))
}
- None -> none
+
+ Absent -> Maybe.absent
}
}
@@ -190,7 +202,7 @@ infix fun <A> Maybe<A>.orElseMaybe(other: Maybe<A>): Maybe<A> = orElseGetMaybe {
*/
inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> =
when (this) {
- is Just -> this
+ is Present -> this
else -> other()
}
@@ -235,7 +247,7 @@ fun <A> Maybe<A>.mergeWith(other: Maybe<A>, transform: (A, A) -> A): Maybe<A> =
inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = buildList {
for (a in this@mapMaybe) {
val result = transform(a)
- if (result is Just) {
+ if (result is Present) {
add(result.value)
}
}
@@ -246,7 +258,7 @@ inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = bu
* the original sequence.
*/
fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> =
- map(transform).filterIsInstance<Just<B>>().map { it.value }
+ map(transform).filterIsInstance<Present<B>>().map { it.value }
/**
* Returns a map with values of only the present results of applying [transform] to each entry in
@@ -256,14 +268,14 @@ inline fun <K, A, B> Map<K, A>.mapMaybeValues(transform: (Map.Entry<K, A>) -> Ma
buildMap {
for (entry in this@mapMaybeValues) {
val result = transform(entry)
- if (result is Just) {
+ if (result is Present) {
put(entry.key, result.value)
}
}
}
/** Returns a map with all non-present values filtered out. */
-fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> =
+fun <K, A> Map<K, Maybe<A>>.filterPresentValues(): Map<K, A> =
asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap()
/**
@@ -277,9 +289,9 @@ fun <A, B> Maybe<Pair<A, B>>.splitPair(): Pair<Maybe<A>, Maybe<B>> =
fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> {
val value = get(key)
if (value == null && !containsKey(key)) {
- return none
+ return Maybe.absent
} else {
@Suppress("UNCHECKED_CAST")
- return just(value as V)
+ return Maybe.present(value as V)
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
index 092dca4d2f1d..fc7b1e05b6a0 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
@@ -16,28 +16,28 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Maybe.Present
/** Contains at least one of two potential values. */
sealed class These<out A, out B> {
- /** Contains a single potential value. */
- class This<A, B> internal constructor(val thiz: A) : These<A, B>()
+ /** A [These] that contains a [First] value. */
+ class First<A, B> internal constructor(val value: A) : These<A, B>()
- /** Contains a single potential value. */
- class That<A, B> internal constructor(val that: B) : These<A, B>()
+ /** A [These] that contains a [Second] value. */
+ class Second<A, B> internal constructor(val value: B) : These<A, B>()
- /** Contains both potential values. */
- class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>()
+ /** A [These] that contains [Both] a [first] and [second] value. */
+ class Both<A, B> internal constructor(val first: A, val second: B) : These<A, B>()
companion object {
- /** Constructs a [These] containing only [thiz]. */
- fun <A> thiz(thiz: A): These<A, Nothing> = This(thiz)
+ /** Constructs a [These] containing the first possibility. */
+ fun <A> first(value: A): These<A, Nothing> = First(value)
- /** Constructs a [These] containing only [that]. */
- fun <B> that(that: B): These<Nothing, B> = That(that)
+ /** Constructs a [These] containing the second possibility. */
+ fun <B> second(value: B): These<Nothing, B> = Second(value)
- /** Constructs a [These] containing both [thiz] and [that]. */
- fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that)
+ /** Constructs a [These] containing both possibilities. */
+ fun <A, B> both(first: A, second: B): These<A, B> = Both(first, second)
}
}
@@ -47,87 +47,88 @@ sealed class These<out A, out B> {
*/
inline fun <A> These<A, A>.merge(f: (A, A) -> A): A =
when (this) {
- is These.This -> thiz
- is These.That -> that
- is These.Both -> f(thiz, that)
+ is These.First -> value
+ is These.Second -> value
+ is These.Both -> f(first, second)
}
-/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */
-fun <A> These<A, *>.maybeThis(): Maybe<A> =
+/** Returns the [These.First] [value][These.First.value] present in this [These] as a [Maybe]. */
+fun <A> These<A, *>.maybeFirst(): Maybe<A> =
when (this) {
- is These.Both -> just(thiz)
- is These.That -> none
- is These.This -> just(thiz)
+ is These.Both -> Maybe.present(first)
+ is These.Second -> Maybe.absent
+ is These.First -> Maybe.present(value)
}
/**
- * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not
+ * Returns the [These.First] [value][These.First.value] present in this [These], or `null` if not
* present.
*/
-fun <A : Any> These<A, *>.thisOrNull(): A? =
+fun <A : Any> These<A, *>.firstOrNull(): A? =
when (this) {
- is These.Both -> thiz
- is These.That -> null
- is These.This -> thiz
+ is These.Both -> first
+ is These.Second -> null
+ is These.First -> value
}
-/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */
-fun <A> These<*, A>.maybeThat(): Maybe<A> =
+/** Returns the [These.Second] [value][These.Second.value] present in this [These] as a [Maybe]. */
+fun <A> These<*, A>.maybeSecond(): Maybe<A> =
when (this) {
- is These.Both -> just(that)
- is These.That -> just(that)
- is These.This -> none
+ is These.Both -> Maybe.present(second)
+ is These.Second -> Maybe.present(value)
+ is These.First -> Maybe.absent
}
/**
- * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not
+ * Returns the [These.Second] [value][These.Second.value] present in this [These], or `null` if not
* present.
*/
-fun <A : Any> These<*, A>.thatOrNull(): A? =
+fun <A : Any> These<*, A>.secondOrNull(): A? =
when (this) {
- is These.Both -> that
- is These.That -> that
- is These.This -> null
+ is These.Both -> second
+ is These.Second -> value
+ is These.First -> null
}
/** Returns [These.Both] values present in this [These] as a [Maybe]. */
fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> =
when (this) {
- is These.Both -> just(thiz to that)
- else -> none
+ is These.Both -> Maybe.present(first to second)
+ else -> Maybe.absent
}
-/** Returns a [These] containing [thiz] and/or [that] if they are present. */
-fun <A, B> these(thiz: Maybe<A>, that: Maybe<B>): Maybe<These<A, B>> =
- when (thiz) {
- is Just ->
- just(
- when (that) {
- is Just -> These.both(thiz.value, that.value)
- else -> These.thiz(thiz.value)
+/** Returns a [These] containing [first] and/or [second] if they are present. */
+fun <A, B> these(first: Maybe<A>, second: Maybe<B>): Maybe<These<A, B>> =
+ when (first) {
+ is Present ->
+ Maybe.present(
+ when (second) {
+ is Present -> These.both(first.value, second.value)
+ else -> These.first(first.value)
}
)
+
else ->
- when (that) {
- is Just -> just(These.that(that.value))
- else -> none
+ when (second) {
+ is Present -> Maybe.present(These.second(second.value))
+ else -> Maybe.absent
}
}
/**
- * Returns a [These] containing [thiz] and/or [that] if they are non-null, or `null` if both are
+ * Returns a [These] containing [first] and/or [second] if they are non-null, or `null` if both are
* `null`.
*/
-fun <A : Any, B : Any> theseNull(thiz: A?, that: B?): These<A, B>? =
- thiz?.let { that?.let { These.both(thiz, that) } ?: These.thiz(thiz) }
- ?: that?.let { These.that(that) }
+fun <A : Any, B : Any> theseNotNull(first: A?, second: B?): These<A, B>? =
+ first?.let { second?.let { These.both(first, second) } ?: These.first(first) }
+ ?: second?.let { These.second(second) }
/**
- * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second]
- * containing all [These.That] values.
+ * Returns two maps, with [Pair.first] containing all [These.First] values and [Pair.second]
+ * containing all [These.Second] values.
*
* If the value is [These.Both], then the associated key with appear in both output maps, bound to
- * [These.Both.thiz] and [These.Both.that] in each respective output.
+ * [These.Both.first] and [These.Both.second] in each respective output.
*/
fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
val a = mutableMapOf<K, A>()
@@ -135,14 +136,14 @@ fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
for ((k, t) in this) {
when (t) {
is These.Both -> {
- a[k] = t.thiz
- b[k] = t.that
+ a[k] = t.first
+ b[k] = t.second
}
- is These.That -> {
- b[k] = t.that
+ is These.Second -> {
+ b[k] = t.value
}
- is These.This -> {
- a[k] = t.thiz
+ is These.First -> {
+ a[k] = t.value
}
}
}
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
new file mode 100644
index 000000000000..88a5b7a4966f
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.util.MapPatch
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeOf
+import com.android.systemui.kairos.util.toMaybe
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class KairosSamples {
+
+ @Test fun test_mapMaybe() = runSample { mapMaybe() }
+
+ fun BuildScope.mapMaybe() {
+ val emitter = MutableEvents<String>()
+ val ints = emitter.mapMaybe { it.toIntOrNull().toMaybe() }
+
+ var observedInput: String? = null
+ emitter.observe { observedInput = it }
+
+ var observedInt: Int? = null
+ ints.observe { observedInt = it }
+
+ launchEffect {
+ // parse succeeds
+ emitter.emit("6")
+ assertEquals(observedInput, "6")
+ assertEquals(observedInt, 6)
+
+ // parse fails
+ emitter.emit("foo")
+ assertEquals(observedInput, "foo")
+ assertEquals(observedInt, 6)
+
+ // parse succeeds
+ emitter.emit("500")
+ assertEquals(observedInput, "500")
+ assertEquals(observedInt, 500)
+ }
+ }
+
+ @Test fun test_mapCheap() = runSample { mapCheap() }
+
+ fun BuildScope.mapCheap() {
+ val emitter = MutableEvents<Int>()
+
+ var invocationCount = 0
+ val squared =
+ emitter.mapCheap {
+ invocationCount++
+ it * it
+ }
+
+ var observedSquare: Int? = null
+ squared.observe { observedSquare = it }
+
+ launchEffect {
+ emitter.emit(10)
+ assertTrue(invocationCount >= 1)
+ assertEquals(observedSquare, 100)
+
+ emitter.emit(2)
+ assertTrue(invocationCount >= 2)
+ assertEquals(observedSquare, 4)
+ }
+ }
+
+ @Test fun test_mapEvents() = runSample { mapEvents() }
+
+ fun BuildScope.mapEvents() {
+ val emitter = MutableEvents<Int>()
+
+ val squared = emitter.map { it * it }
+
+ var observedSquare: Int? = null
+ squared.observe { observedSquare = it }
+
+ launchEffect {
+ emitter.emit(10)
+ assertEquals(observedSquare, 100)
+
+ emitter.emit(2)
+ assertEquals(observedSquare, 4)
+ }
+ }
+
+ @Test fun test_eventsLoop() = runSample { eventsLoop() }
+
+ fun BuildScope.eventsLoop() {
+ val emitter = MutableEvents<Unit>()
+ var newCount: Events<Int> by EventsLoop()
+ val count = newCount.holdState(0)
+ newCount = emitter.map { count.sample() + 1 }
+
+ var observedCount = 0
+ count.observe { observedCount = it }
+
+ launchEffect {
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 1)
+
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 2)
+ }
+ }
+
+ @Test fun test_stateLoop() = runSample { stateLoop() }
+
+ fun BuildScope.stateLoop() {
+ val emitter = MutableEvents<Unit>()
+ var count: State<Int> by StateLoop()
+ count = emitter.map { count.sample() + 1 }.holdState(0)
+
+ var observedCount = 0
+ count.observe { observedCount = it }
+
+ launchEffect {
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 1)
+
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 2)
+ }
+ }
+
+ @Test fun test_changes() = runSample { changes() }
+
+ fun BuildScope.changes() {
+ val emitter = MutableEvents<Int>()
+ val state = emitter.holdState(0)
+
+ var numEmissions = 0
+ emitter.observe { numEmissions++ }
+
+ var observedState = 0
+ var numChangeEmissions = 0
+ state.changes.observe {
+ observedState = it
+ numChangeEmissions++
+ }
+
+ launchEffect {
+ emitter.emit(0)
+ assertEquals(numEmissions, expected = 1)
+ assertEquals(numChangeEmissions, expected = 0)
+ assertEquals(observedState, expected = 0)
+
+ emitter.emit(5)
+ assertEquals(numEmissions, expected = 2)
+ assertEquals(numChangeEmissions, expected = 1)
+ assertEquals(observedState, expected = 5)
+
+ emitter.emit(3)
+ assertEquals(numEmissions, expected = 3)
+ assertEquals(numChangeEmissions, expected = 2)
+ assertEquals(observedState, expected = 3)
+
+ emitter.emit(3)
+ assertEquals(numEmissions, expected = 4)
+ assertEquals(numChangeEmissions, expected = 2)
+ assertEquals(observedState, expected = 3)
+
+ emitter.emit(5)
+ assertEquals(numEmissions, expected = 5)
+ assertEquals(numChangeEmissions, expected = 3)
+ assertEquals(observedState, expected = 5)
+ }
+ }
+
+ @Test fun test_partitionThese() = runSample { partitionThese() }
+
+ fun BuildScope.partitionThese() {
+ val emitter = MutableEvents<These<Int, String>>()
+ val (lefts, rights) = emitter.partitionThese()
+
+ var observedLeft: Int? = null
+ lefts.observe { observedLeft = it }
+
+ var observedRight: String? = null
+ rights.observe { observedRight = it }
+
+ launchEffect {
+ emitter.emit(These.first(10))
+ assertEquals(observedLeft, 10)
+ assertEquals(observedRight, null)
+
+ emitter.emit(These.both(2, "foo"))
+ assertEquals(observedLeft, 2)
+ assertEquals(observedRight, "foo")
+
+ emitter.emit(These.second("bar"))
+ assertEquals(observedLeft, 2)
+ assertEquals(observedRight, "bar")
+ }
+ }
+
+ @Test fun test_merge() = runSample { merge() }
+
+ fun BuildScope.merge() {
+ val emitter = MutableEvents<Int>()
+ val fizz = emitter.mapNotNull { if (it % 3 == 0) "Fizz" else null }
+ val buzz = emitter.mapNotNull { if (it % 5 == 0) "Buzz" else null }
+ val fizzbuzz = fizz.mergeWith(buzz) { _, _ -> "Fizz Buzz" }
+ val output = mergeLeft(fizzbuzz, emitter.mapCheap { it.toString() })
+
+ var observedOutput: String? = null
+ output.observe { observedOutput = it }
+
+ launchEffect {
+ emitter.emit(1)
+ assertEquals(observedOutput, "1")
+ emitter.emit(2)
+ assertEquals(observedOutput, "2")
+ emitter.emit(3)
+ assertEquals(observedOutput, "Fizz")
+ emitter.emit(4)
+ assertEquals(observedOutput, "4")
+ emitter.emit(5)
+ assertEquals(observedOutput, "Buzz")
+ emitter.emit(6)
+ assertEquals(observedOutput, "Fizz")
+ emitter.emit(15)
+ assertEquals(observedOutput, "Fizz Buzz")
+ }
+ }
+
+ @Test fun test_groupByKey() = runSample { groupByKey() }
+
+ fun BuildScope.groupByKey() {
+ val emitter = MutableEvents<Map<String, Int>>()
+ val grouped = emitter.groupByKey()
+ val groupA = grouped["A"]
+ val groupB = grouped["B"]
+
+ var numEmissions = 0
+ emitter.observe { numEmissions++ }
+
+ var observedA: Int? = null
+ groupA.observe { observedA = it }
+
+ var observedB: Int? = null
+ groupB.observe { observedB = it }
+
+ launchEffect {
+ // emit to group A
+ emitter.emit(mapOf("A" to 3))
+ assertEquals(numEmissions, 1)
+ assertEquals(observedA, 3)
+ assertEquals(observedB, null)
+
+ // emit to groups B and C, even though there are no observers of C
+ emitter.emit(mapOf("B" to 9, "C" to 100))
+ assertEquals(numEmissions, 2)
+ assertEquals(observedA, 3)
+ assertEquals(observedB, 9)
+
+ // emit to groups A and B
+ emitter.emit(mapOf("B" to 6, "A" to 14))
+ assertEquals(numEmissions, 3)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+
+ // emit to group with no listeners
+ emitter.emit(mapOf("Q" to -66))
+ assertEquals(numEmissions, 4)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+
+ // no-op emission
+ emitter.emit(emptyMap())
+ assertEquals(numEmissions, 5)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+ }
+ }
+
+ @Test fun test_switchEvents() = runSample { switchEvents() }
+
+ fun BuildScope.switchEvents() {
+ val negator = MutableEvents<Unit>()
+ val emitter = MutableEvents<Int>()
+ val negate = negator.foldState(false) { _, negate -> !negate }
+ val output =
+ negate.map { negate -> if (negate) emitter.map { it * -1 } else emitter }.switchEvents()
+
+ var observed: Int? = null
+ output.observe { observed = it }
+
+ launchEffect {
+ // emit like normal
+ emitter.emit(10)
+ assertEquals(observed, 10)
+
+ // enable negation
+ observed = null
+ negator.emit(Unit)
+ assertEquals(observed, null)
+
+ emitter.emit(99)
+ assertEquals(observed, -99)
+
+ // disable negation
+ observed = null
+ negator.emit(Unit)
+ emitter.emit(7)
+ assertEquals(observed, 7)
+ }
+ }
+
+ @Test fun test_switchEventsPromptly() = runSample { switchEventsPromptly() }
+
+ fun BuildScope.switchEventsPromptly() {
+ val emitter = MutableEvents<Int>()
+ val enabled = emitter.map { it > 10 }.holdState(false)
+ val switchedIn = enabled.map { enabled -> if (enabled) emitter else emptyEvents }
+ val deferredSwitch = switchedIn.switchEvents()
+ val promptSwitch = switchedIn.switchEventsPromptly()
+
+ var observedDeferred: Int? = null
+ deferredSwitch.observe { observedDeferred = it }
+
+ var observedPrompt: Int? = null
+ promptSwitch.observe { observedPrompt = it }
+
+ launchEffect {
+ emitter.emit(3)
+ assertEquals(observedDeferred, null)
+ assertEquals(observedPrompt, null)
+
+ emitter.emit(20)
+ assertEquals(observedDeferred, null)
+ assertEquals(observedPrompt, 20)
+
+ emitter.emit(30)
+ assertEquals(observedDeferred, 30)
+ assertEquals(observedPrompt, 30)
+
+ emitter.emit(8)
+ assertEquals(observedDeferred, 8)
+ assertEquals(observedPrompt, 8)
+
+ emitter.emit(1)
+ assertEquals(observedDeferred, 8)
+ assertEquals(observedPrompt, 8)
+ }
+ }
+
+ @Test fun test_sampleTransactional() = runSample { sampleTransactional() }
+
+ fun BuildScope.sampleTransactional() {
+ var store = 0
+ val transactional = transactionally { store++ }
+
+ effect {
+ assertEquals(store, 0)
+ assertEquals(transactional.sample(), 0)
+ assertEquals(store, 1)
+ assertEquals(transactional.sample(), 0)
+ assertEquals(store, 1)
+ }
+ }
+
+ @Test fun test_states() = runSample { states() }
+
+ fun BuildScope.states() {
+ val constantState = stateOf(10)
+ effect { assertEquals(constantState.sample(), 10) }
+
+ val mappedConstantState: State<Int> = constantState.map { it * 2 }
+ effect { assertEquals(mappedConstantState.sample(), 20) }
+
+ val emitter = MutableEvents<Int>()
+ val heldState: State<Int?> = emitter.holdState(null)
+ effect { assertEquals(heldState.sample(), null) }
+
+ var observed: Int? = null
+ var wasObserved = false
+ heldState.observe {
+ observed = it
+ wasObserved = true
+ }
+ launchEffect {
+ assertTrue(wasObserved)
+ emitter.emit(4)
+ assertEquals(observed, 4)
+ }
+
+ val combinedStates: State<Pair<Int, Int?>> =
+ combine(mappedConstantState, heldState) { a, b -> Pair(a, b) }
+
+ effect { assertEquals(combinedStates.sample(), 20 to null) }
+
+ var observedPair: Pair<Int, Int?>? = null
+ combinedStates.observe { observedPair = it }
+ launchEffect {
+ emitter.emit(12)
+ assertEquals(observedPair, 20 to 12)
+ }
+ }
+
+ @Test fun test_holdState() = runSample { holdState() }
+
+ fun BuildScope.holdState() {
+ val emitter = MutableEvents<Int>()
+ val heldState: State<Int?> = emitter.holdState(null)
+ effect { assertEquals(heldState.sample(), null) }
+
+ var observed: Int? = null
+ var wasObserved = false
+ heldState.observe {
+ observed = it
+ wasObserved = true
+ }
+ launchEffect {
+ // observation of the initial state took place immediately
+ assertTrue(wasObserved)
+
+ // state changes are also observed
+ emitter.emit(4)
+ assertEquals(observed, 4)
+
+ emitter.emit(20)
+ assertEquals(observed, 20)
+ }
+ }
+
+ @Test fun test_mapState() = runSample { mapState() }
+
+ fun BuildScope.mapState() {
+ val emitter = MutableEvents<Int>()
+ val held: State<Int> = emitter.holdState(0)
+ val squared: State<Int> = held.map { it * it }
+
+ var observed: Int? = null
+ squared.observe { observed = it }
+
+ launchEffect {
+ assertEquals(observed, 0)
+
+ emitter.emit(10)
+ assertEquals(observed, 100)
+ }
+ }
+
+ @Test fun test_combineState() = runSample { combineState() }
+
+ fun BuildScope.combineState() {
+ val emitter = MutableEvents<Int>()
+ val state = emitter.holdState(0)
+ val squared = state.map { it * it }
+ val negated = state.map { -it }
+ val combined = squared.combine(negated) { a, b -> Pair(a, b) }
+
+ val observed = mutableListOf<Pair<Int, Int>>()
+ combined.observe { observed.add(it) }
+
+ launchEffect {
+ emitter.emit(10)
+ emitter.emit(20)
+ emitter.emit(3)
+
+ assertEquals(observed, listOf(0 to 0, 100 to -10, 400 to -20, 9 to -3))
+ }
+ }
+
+ @Test fun test_flatMap() = runSample { flatMap() }
+
+ fun BuildScope.flatMap() {
+ val toggler = MutableEvents<Unit>()
+ val firstEmitter = MutableEvents<Unit>()
+ val secondEmitter = MutableEvents<Unit>()
+
+ val firstCount: State<Int> = firstEmitter.foldState(0) { _, count -> count + 1 }
+ val secondCount: State<Int> = secondEmitter.foldState(0) { _, count -> count + 1 }
+ val toggleState: State<Boolean> = toggler.foldState(true) { _, state -> !state }
+
+ val activeCount: State<Int> =
+ toggleState.flatMap { b -> if (b) firstCount else secondCount }
+
+ var observed: Int? = null
+ activeCount.observe { observed = it }
+
+ launchEffect {
+ assertEquals(observed, 0)
+
+ firstEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ secondEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ secondEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ toggler.emit(Unit)
+ assertEquals(observed, 2)
+
+ toggler.emit(Unit)
+ assertEquals(observed, 1)
+ }
+ }
+
+ @Test fun test_incrementals() = runSample { incrementals() }
+
+ fun BuildScope.incrementals() {
+ val patchEmitter = MutableEvents<MapPatch<String, Int>>()
+ val incremental: Incremental<String, Int> = patchEmitter.foldStateMapIncrementally()
+ val squared = incremental.mapValues { (key, value) -> value * value }
+
+ var observedUpdate: MapPatch<String, Int>? = null
+ squared.updates.observe { observedUpdate = it }
+
+ var observedState: Map<String, Int>? = null
+ squared.observe { observedState = it }
+
+ launchEffect {
+ assertEquals(observedState, emptyMap())
+ assertEquals(observedUpdate, null)
+
+ // add entry: A => 10
+ patchEmitter.emit(mapOf("A" to maybeOf(10)))
+ assertEquals(observedState, mapOf("A" to 100))
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(100)))
+
+ // update entry: A => 5
+ // add entry: B => 6
+ patchEmitter.emit(mapOf("A" to maybeOf(5), "B" to maybeOf(6)))
+ assertEquals(observedState, mapOf("A" to 25, "B" to 36))
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(25), "B" to maybeOf(36)))
+
+ // remove entry: A
+ // add entry: C => 9
+ // remove non-existent entry: F
+ patchEmitter.emit(mapOf("A" to maybeOf(), "C" to maybeOf(9), "F" to maybeOf()))
+ assertEquals(observedState, mapOf("B" to 36, "C" to 81))
+ // non-existent entry is filtered from the update
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(), "C" to maybeOf(81)))
+ }
+ }
+
+ @Test fun test_mergeEventsIncrementally() = runSample(block = mergeEventsIncrementally())
+
+ fun mergeEventsIncrementally(): BuildSpec<Unit> = buildSpec {
+ val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+ val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+ val merged: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+
+ var observed: Map<String, Int>? = null
+ merged.observe { observed = it }
+
+ launchEffect {
+ // add events entry: A
+ val emitterA = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+ emitterA.emit(100)
+ assertEquals(observed, mapOf("A" to 100))
+
+ // add events entry: B
+ val emitterB = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+ // merged emits from both A and B
+ emitterB.emit(5)
+ assertEquals(observed, mapOf("B" to 5))
+
+ emitterA.emit(20)
+ assertEquals(observed, mapOf("A" to 20))
+
+ // remove entry: A
+ patchEmitter.emit(mapOf("A" to maybeOf()))
+ emitterA.emit(0)
+ // event is not emitted now that A has been removed
+ assertEquals(observed, mapOf("A" to 20))
+
+ // but B still works
+ emitterB.emit(3)
+ assertEquals(observed, mapOf("B" to 3))
+ }
+ }
+
+ @Test
+ fun test_mergeEventsIncrementallyPromptly() =
+ runSample(block = mergeEventsIncrementallyPromptly())
+
+ fun mergeEventsIncrementallyPromptly(): BuildSpec<Unit> = buildSpec {
+ val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+ val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+ val deferredMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+ val promptMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementallyPromptly()
+
+ var observedDeferred: Map<String, Int>? = null
+ deferredMerge.observe { observedDeferred = it }
+
+ var observedPrompt: Map<String, Int>? = null
+ promptMerge.observe { observedPrompt = it }
+
+ launchEffect {
+ val emitterA = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+ emitterA.emit(100)
+ assertEquals(observedDeferred, mapOf("A" to 100))
+ assertEquals(observedPrompt, mapOf("A" to 100))
+
+ val emitterB = patchEmitter.map { 5 }
+ patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+ assertEquals(observedDeferred, mapOf("A" to 100))
+ assertEquals(observedPrompt, mapOf("B" to 5))
+ }
+ }
+
+ @Test fun test_applyLatestStateful() = runSample(block = applyLatestStateful())
+
+ fun applyLatestStateful(): BuildSpec<Unit> = buildSpec {
+ val reset = MutableEvents<Unit>()
+ val emitter = MutableEvents<Unit>()
+ val stateEvents: Events<State<Int>> =
+ reset
+ .map { statefully { emitter.foldState(0) { _, count -> count + 1 } } }
+ .applyLatestStateful()
+ val activeState: State<State<Int>?> = stateEvents.holdState(null)
+
+ launchEffect {
+ // nothing is active yet
+ kairosNetwork.transact { assertEquals(activeState.sample(), null) }
+
+ // activate the counter
+ reset.emit(Unit)
+ val firstState =
+ kairosNetwork.transact {
+ assertEquals(activeState.sample()?.sample(), 0)
+ activeState.sample()!!
+ }
+
+ // emit twice
+ emitter.emit(Unit)
+ emitter.emit(Unit)
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+ // start a new counter, disabling the old one
+ reset.emit(Unit)
+ val secondState =
+ kairosNetwork.transact {
+ assertEquals(activeState.sample()?.sample(), 0)
+ activeState.sample()!!
+ }
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+ // emit: the new counter updates, but the old one does not
+ emitter.emit(Unit)
+ kairosNetwork.transact { assertEquals(secondState.sample(), 1) }
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+ }
+ }
+
+ @Test fun test_applyLatestStatefulForKey() = runSample(block = applyLatestStatefulForKey())
+
+ fun applyLatestStatefulForKey(): BuildSpec<Unit> = buildSpec {
+ val reset = MutableEvents<String>()
+ val emitter = MutableEvents<String>()
+ val stateEvents: Events<MapPatch<String, State<Int>>> =
+ reset
+ .map { key ->
+ mapOf(
+ key to
+ maybeOf(
+ statefully {
+ emitter
+ .filter { it == key }
+ .foldState(0) { _, count -> count + 1 }
+ }
+ )
+ )
+ }
+ .applyLatestStatefulForKey()
+ val activeStatesByKey: Incremental<String, State<Int>> =
+ stateEvents.foldStateMapIncrementally(emptyMap())
+
+ launchEffect {
+ // nothing is active yet
+ kairosNetwork.transact { assertEquals(activeStatesByKey.sample(), emptyMap()) }
+
+ // activate a new entry A
+ reset.emit("A")
+ val firstStateA =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A"))
+ stateMap.getValue("A").also { assertEquals(it.sample(), 0) }
+ }
+
+ // emit twice to A
+ emitter.emit("A")
+ emitter.emit("A")
+ kairosNetwork.transact { assertEquals(firstStateA.sample(), 2) }
+
+ // active a new entry B
+ reset.emit("B")
+ val firstStateB =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A", "B"))
+ stateMap.getValue("B").also {
+ assertEquals(it.sample(), 0)
+ assertEquals(firstStateA.sample(), 2)
+ }
+ }
+
+ // emit once to B
+ emitter.emit("B")
+ kairosNetwork.transact {
+ assertEquals(firstStateA.sample(), 2)
+ assertEquals(firstStateB.sample(), 1)
+ }
+
+ // activate a new entry for A, disabling the old entry
+ reset.emit("A")
+ val secondStateA =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A", "B"))
+ stateMap.getValue("A").also {
+ assertEquals(it.sample(), 0)
+ assertEquals(firstStateB.sample(), 1)
+ }
+ }
+
+ // emit to A: the new A state updates, but the old one does not
+ emitter.emit("A")
+ kairosNetwork.transact {
+ assertEquals(firstStateA.sample(), 2)
+ assertEquals(secondStateA.sample(), 1)
+ }
+ }
+ }
+
+ private fun runSample(
+ dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+ block: BuildScope.() -> Unit,
+ ) {
+ runTest(dispatcher, timeout = 1.seconds) {
+ val kairosNetwork = backgroundScope.launchKairosNetwork()
+ backgroundScope.launch { kairosNetwork.activateSpec { block() } }
+ }
+ }
+}
+
+private fun <T> assertEquals(actual: T, expected: T) = Assert.assertEquals(expected, actual)
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
index 150b462df655..ffe6e955e884 100644
--- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
@@ -1,14 +1,12 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.maybe
-import com.android.systemui.kairos.util.none
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
@@ -142,7 +140,7 @@ class KairosTests {
// convert Eventss to States so that they can be combined
val combined =
- left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r ->
+ left.holdState("left" to 0).combine(right.holdState("right" to 0)) { l, r ->
l to r
}
combined.changes // get State changes
@@ -590,7 +588,7 @@ class KairosTests {
intStopEmitter.emit(Unit) // intAH.complete()
runCurrent()
- // assertEquals(just(10), network.await())
+ // assertEquals(present(10), network.await())
}
@Test
@@ -692,12 +690,12 @@ class KairosTests {
}
runCurrent()
- emitter.emit(Left(10))
+ emitter.emit(First(10))
runCurrent()
assertEquals(20, result.value)
- emitter.emit(Right(30))
+ emitter.emit(Second(30))
runCurrent()
assertEquals(-30, result.value)
@@ -908,7 +906,7 @@ class KairosTests {
activateSpecWithResult(network) {
val bA = updater.map { it * 2 }.holdState(0)
val bB = updater.holdState(0)
- val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b }
+ val combineD: State<Pair<Int, Int>> = bA.combine(bB) { a, b -> a to b }
val sampleS = emitter.sample(combineD) { _, b -> b }
sampleS.nextDeferred()
}
@@ -1142,16 +1140,13 @@ class KairosTests {
val eRemoved =
childChangeById
.eventsForKey(childId)
- .filter { it === None }
+ .filter { it === Absent }
.onEach {
println(
"removing? (groupId=$groupId, childId=$childId)"
)
}
- .nextOnly(
- name =
- "eRemoved(groupId=$groupId, childId=$childId)"
- )
+ .nextOnly()
val addChild: Events<Maybe<State<String>>> =
now.map { mChild }
@@ -1168,13 +1163,9 @@ class KairosTests {
"removeChild (groupId=$groupId, childId=$childId)"
)
}
- .map { none() }
+ .map { Maybe.absent() }
- addChild.mergeWith(
- removeChild,
- name =
- "childUpdatesMerged(groupId=$groupId, childId=$childId)",
- ) { _, _ ->
+ addChild.mergeWith(removeChild) { _, _ ->
error("unexpected coincidence")
}
}
@@ -1182,7 +1173,7 @@ class KairosTests {
}
val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> =
map.onEach { println("merge patch: $it") }
- .mergeIncrementallyPromptly(name = "mergeIncrementally")
+ .mergeEventsIncrementallyPromptly()
mergeIncrementally
.onEach { println("foldmap patch: $it") }
.foldStateMapIncrementally()
@@ -1203,14 +1194,14 @@ class KairosTests {
val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>()
println()
println("init outer 0")
- e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") })))
+ e.emit(mapOf(0 to Maybe.present(emitter2.onEach { println("emitter2 emit: $it") })))
runCurrent()
assertEquals(mapOf(0 to emptyMap()), state.value)
println()
println("init inner 10")
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(0, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(0, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value)
@@ -1218,19 +1209,19 @@ class KairosTests {
// replace
println()
println("replace inner 10")
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(1, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value)
// remove
- emitter2.emit(mapOf(10 to none()))
+ emitter2.emit(mapOf(10 to Maybe.absent()))
runCurrent()
assertEquals(mapOf(0 to emptyMap()), state.value)
// add again
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(2, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
@@ -1242,9 +1233,9 @@ class KairosTests {
// batch update
emitter2.emit(
mapOf(
- 10 to none(),
- 11 to just(MutableStateFlow("(0, 11)")),
- 12 to just(MutableStateFlow("(0, 12)")),
+ 10 to Maybe.absent(),
+ 11 to Maybe.present(MutableStateFlow("(0, 11)")),
+ 12 to Maybe.present(MutableStateFlow("(0, 12)")),
)
)
runCurrent()
@@ -1278,7 +1269,7 @@ class KairosTests {
}
var outerCount = 0
- val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+ val lastEvent: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
flowOfFlows
.map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
.pairwise(MutableStateFlow(null))
@@ -1296,18 +1287,18 @@ class KairosTests {
assertEquals(1, outerCount)
// assertEquals(1, incCount.subscriptionCount)
- assertNull(laseventss.value.second.value)
+ assertNull(lastEvent.value.second.value)
incCount.emit(Unit)
runCurrent()
println("checking")
- assertEquals(1, laseventss.value.second.value)
+ assertEquals(1, lastEvent.value.second.value)
incCount.emit(Unit)
runCurrent()
- assertEquals(2, laseventss.value.second.value)
+ assertEquals(2, lastEvent.value.second.value)
newCount.emit(newFlow())
runCurrent()
@@ -1315,9 +1306,9 @@ class KairosTests {
runCurrent()
// verify old flow is not getting updates
- assertEquals(2, laseventss.value.first.value)
+ assertEquals(2, lastEvent.value.first.value)
// but the new one is
- assertEquals(1, laseventss.value.second.value)
+ assertEquals(1, lastEvent.value.second.value)
}
@Test
@@ -1326,7 +1317,7 @@ class KairosTests {
var observedCount: Int? = null
activateSpec(network) {
val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } }
- deferredBuildScopeAction { c.get().observe { observedCount = it } }
+ deferredBuildScopeAction { c.value.observe { observedCount = it } }
}
runCurrent()
assertEquals(0, observedCount)
@@ -1385,7 +1376,7 @@ class KairosTests {
activateSpec(network) {
val handle =
input.observe {
- effectCoroutineScope.launch {
+ launch {
runningCount++
awaitClose { runningCount-- }
}
@@ -1420,7 +1411,7 @@ class KairosTests {
val specJob =
activateSpec(network) {
input.takeUntil(stopper).observe {
- effectCoroutineScope.launch {
+ launch {
runningCount++
awaitClose { runningCount-- }
}
diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp
index ac545dfb06cc..c4086c5b3835 100644
--- a/ravenwood/tests/bivalenttest/Android.bp
+++ b/ravenwood/tests/bivalenttest/Android.bp
@@ -40,6 +40,7 @@ java_defaults {
"junit-params",
"platform-parametric-runner-lib",
+ "platform-compat-test-rules",
// To make sure it won't cause VerifyError (b/324063814)
"platformprotosnano",
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
index 882c91c43ee9..540b0822319c 100644
--- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
@@ -16,31 +16,52 @@
package com.android.ravenwoodtest.bivalenttest.compat
import android.app.compat.CompatChanges
+import android.compat.testing.PlatformCompatChangeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.ravenwood.RavenwoodEnvironment.CompatIdsForTest
-import org.junit.Assert
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RavenwoodCompatFrameworkTest {
+
+ @get:Rule
+ val compatRule = PlatformCompatChangeRule()
+
@Test
fun testEnabled() {
- Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1))
+ assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1))
}
@Test
fun testDisabled() {
- Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_2))
+ assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_2))
}
@Test
fun testEnabledAfterSForUApps() {
- Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_3))
+ assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_3))
}
@Test
fun testEnabledAfterUForUApps() {
- Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4))
+ assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4))
+ }
+
+ @Test
+ @EnableCompatChanges(CompatIdsForTest.TEST_COMPAT_ID_5)
+ fun testEnableCompatChanges() {
+ assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_5))
+ }
+
+ @Test
+ @DisableCompatChanges(CompatIdsForTest.TEST_COMPAT_ID_5)
+ fun testDisableCompatChanges() {
+ assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_5))
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
index a69ececd5373..e3d7062ddb4e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
@@ -18,6 +18,8 @@ package com.android.server.accessibility;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
+
import android.accessibilityservice.AccessibilityTrace;
import android.annotation.NonNull;
import android.content.ContentResolver;
@@ -286,8 +288,6 @@ public class AutoclickController extends BaseEventStreamTransformation {
}
public void update() {
- // TODO(b/383901288): update delay time once determined by UX.
- long SHOW_INDICATOR_DELAY_TIME = 150;
long scheduledShowIndicatorTime =
SystemClock.uptimeMillis() + SHOW_INDICATOR_DELAY_TIME;
// If there already is a scheduled show indicator at time before the updated time, just
@@ -432,6 +432,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
*/
public void updateDelay(int delay) {
mDelay = delay;
+
+ if (Flags.enableAutoclickIndicator() && mAutoclickIndicatorView != null) {
+ mAutoclickIndicatorView.setAnimationDuration(delay - SHOW_INDICATOR_DELAY_TIME);
+ }
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
index 54c31e5bca79..816d8e456a9a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
@@ -16,25 +16,43 @@
package com.android.server.accessibility;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.RectF;
import android.util.DisplayMetrics;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
// A visual indicator for the autoclick feature.
public class AutoclickIndicatorView extends View {
private static final String TAG = AutoclickIndicatorView.class.getSimpleName();
+ // TODO(b/383901288): update delay time once determined by UX.
+ static final int SHOW_INDICATOR_DELAY_TIME = 150;
+
+ static final int MINIMAL_ANIMATION_DURATION = 50;
+
// TODO(b/383901288): allow users to customize the indicator area.
static final float RADIUS = 50;
private final Paint mPaint;
+ private final ValueAnimator mAnimator;
+
+ private final RectF mRingRect;
+
// x and y coordinates of the visual indicator.
private float mX;
private float mY;
+ // Current sweep angle of the animated ring.
+ private float mSweepAngle;
+
+ private int mAnimationDuration = AccessibilityManager.AUTOCLICK_DELAY_DEFAULT;
+
// Status of whether the visual indicator should display or not.
private boolean showIndicator = false;
@@ -46,6 +64,18 @@ public class AutoclickIndicatorView extends View {
mPaint.setARGB(255, 52, 103, 235);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
+
+ mAnimator = ValueAnimator.ofFloat(0, 360);
+ mAnimator.setDuration(mAnimationDuration);
+ mAnimator.setInterpolator(new LinearInterpolator());
+ mAnimator.addUpdateListener(
+ animation -> {
+ mSweepAngle = (float) animation.getAnimatedValue();
+ // Redraw the view with the updated angle.
+ invalidate();
+ });
+
+ mRingRect = new RectF();
}
@Override
@@ -53,7 +83,12 @@ public class AutoclickIndicatorView extends View {
super.onDraw(canvas);
if (showIndicator) {
- canvas.drawCircle(mX, mY, RADIUS, mPaint);
+ mRingRect.set(
+ /* left= */ mX - RADIUS,
+ /* top= */ mY - RADIUS,
+ /* right= */ mX + RADIUS,
+ /* bottom= */ mY + RADIUS);
+ canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint);
}
}
@@ -75,10 +110,17 @@ public class AutoclickIndicatorView extends View {
public void redrawIndicator() {
showIndicator = true;
invalidate();
+ mAnimator.start();
}
public void clearIndicator() {
showIndicator = false;
+ mAnimator.cancel();
invalidate();
}
+
+ public void setAnimationDuration(int duration) {
+ mAnimationDuration = Math.max(duration, MINIMAL_ANIMATION_DURATION);
+ mAnimator.setDuration(mAnimationDuration);
+ }
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 1f3b31692289..aeb2f5e9be84 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -176,7 +176,7 @@ public class VirtualDeviceManagerService extends SystemService {
public VirtualDeviceManagerService(Context context) {
super(context);
mImpl = new VirtualDeviceManagerImpl();
- mNativeImpl = Flags.enableNativeVdm() ? new VirtualDeviceManagerNativeImpl() : null;
+ mNativeImpl = new VirtualDeviceManagerNativeImpl();
mLocalService = new LocalService();
}
@@ -208,9 +208,7 @@ public class VirtualDeviceManagerService extends SystemService {
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public void onStart() {
publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl);
- if (Flags.enableNativeVdm()) {
- publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl);
- }
+ publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl);
publishLocalService(VirtualDeviceManagerInternal.class, mLocalService);
ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService(
ActivityTaskManagerInternal.class);
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 41b4cbd9e074..6cca7d16842a 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8950,18 +8950,12 @@ public final class ActiveServices {
if (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk) {
return true; // In this case, we only check the service's target SDK level.
}
- final int callingUid;
- if (Flags.newFgsRestrictionLogic()) {
- // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
- if (actualCallingUid == Process.SYSTEM_UID) {
- return true;
- }
- callingUid = actualCallingUid;
- } else {
- // Legacy logic used mRecentCallingUid.
- callingUid = r.mRecentCallingUid;
+ // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
+ if (actualCallingUid == Process.SYSTEM_UID) {
+ return true;
}
- if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, callingUid)) {
+ if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID,
+ actualCallingUid)) {
return false; // If the caller targets < S, then we still disable the restrictions.
}
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 92d33c9eae56..ca34a13c55b1 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -278,24 +278,21 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
* Whether to use the new "while-in-use permission" logic for FGS start
*/
private boolean useNewWiuLogic_forStart() {
- return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
- && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
+ return CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
}
/**
* Whether to use the new "while-in-use permission" logic for capabilities
*/
private boolean useNewWiuLogic_forCapabilities() {
- return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
- && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
+ return CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
}
/**
* Whether to use the new "FGS BG start exemption" logic.
*/
private boolean useNewBfslLogic() {
- return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
- && CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
+ return CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c31b9ef60bd2..ec74f60539a2 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -160,6 +160,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -1920,8 +1921,14 @@ class UserController implements Handler.Callback {
return false;
}
- mHandler.post(() -> startUserInternalOnHandler(userId, oldUserId, userStartMode,
- unlockListener, callingUid, callingPid));
+ final Runnable continueStartUserInternal = () -> continueStartUserInternal(userInfo,
+ oldUserId, userStartMode, unlockListener, callingUid, callingPid);
+ if (foreground) {
+ mHandler.post(() -> dispatchOnBeforeUserSwitching(userId, () ->
+ mHandler.post(continueStartUserInternal)));
+ } else {
+ continueStartUserInternal.run();
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1929,11 +1936,11 @@ class UserController implements Handler.Callback {
return true;
}
- private void startUserInternalOnHandler(int userId, int oldUserId, int userStartMode,
+ private void continueStartUserInternal(UserInfo userInfo, int oldUserId, int userStartMode,
IProgressListener unlockListener, int callingUid, int callingPid) {
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
final boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
- final UserInfo userInfo = getUserInfo(userId);
+ final int userId = userInfo.id;
boolean needStart = false;
boolean updateUmState = false;
@@ -1995,7 +2002,6 @@ class UserController implements Handler.Callback {
// it should be moved outside, but for now it's not as there are many calls to
// external components here afterwards
updateProfileRelatedCaches();
- dispatchOnBeforeUserSwitching(userId);
mInjector.getWindowManager().setCurrentUser(userId);
mInjector.reportCurWakefulnessUsageEvent();
// Once the internal notion of the active user has switched, we lock the device
@@ -2296,25 +2302,42 @@ class UserController implements Handler.Callback {
mUserSwitchObservers.finishBroadcast();
}
- private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId) {
+ private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId, Runnable onComplete) {
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("dispatchOnBeforeUserSwitching-" + newUserId);
- final int observerCount = mUserSwitchObservers.beginBroadcast();
- for (int i = 0; i < observerCount; i++) {
- final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
- t.traceBegin("onBeforeUserSwitching-" + name);
+ final AtomicBoolean isFirst = new AtomicBoolean(true);
+ startTimeoutForOnBeforeUserSwitching(isFirst, onComplete);
+ informUserSwitchObservers((observer, callback) -> {
try {
- mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
+ observer.onBeforeUserSwitching(newUserId, callback);
} catch (RemoteException e) {
- // Ignore
- } finally {
- t.traceEnd();
+ // ignore
}
- }
- mUserSwitchObservers.finishBroadcast();
+ }, () -> {
+ if (isFirst.getAndSet(false)) {
+ onComplete.run();
+ }
+ }, "onBeforeUserSwitching");
t.traceEnd();
}
+ private void startTimeoutForOnBeforeUserSwitching(AtomicBoolean isFirst,
+ Runnable onComplete) {
+ final long timeout = getUserSwitchTimeoutMs();
+ mHandler.postDelayed(() -> {
+ if (isFirst.getAndSet(false)) {
+ String unresponsiveObservers;
+ synchronized (mLock) {
+ unresponsiveObservers = String.join(", ", mCurWaitingUserSwitchCallbacks);
+ }
+ Slogf.e(TAG, "Timeout on dispatchOnBeforeUserSwitching. These UserSwitchObservers "
+ + "did not respond in " + timeout + "ms: " + unresponsiveObservers + ".");
+ onComplete.run();
+ }
+ }, timeout);
+ }
+
+
/** Called on handler thread */
@VisibleForTesting
void dispatchUserSwitchComplete(@UserIdInt int oldUserId, @UserIdInt int newUserId) {
@@ -2527,70 +2550,76 @@ class UserController implements Handler.Callback {
t.traceBegin("dispatchUserSwitch-" + oldUserId + "-to-" + newUserId);
EventLog.writeEvent(EventLogTags.UC_DISPATCH_USER_SWITCH, oldUserId, newUserId);
+ uss.switching = true;
+ informUserSwitchObservers((observer, callback) -> {
+ try {
+ observer.onUserSwitching(newUserId, callback);
+ } catch (RemoteException e) {
+ // ignore
+ }
+ }, () -> {
+ synchronized (mLock) {
+ sendContinueUserSwitchLU(uss, oldUserId, newUserId);
+ }
+ }, "onUserSwitching");
+ t.traceEnd();
+ }
+ void informUserSwitchObservers(BiConsumer<IUserSwitchObserver, IRemoteCallback> consumer,
+ final Runnable onComplete, String trace) {
final int observerCount = mUserSwitchObservers.beginBroadcast();
- if (observerCount > 0) {
- final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
+ if (observerCount == 0) {
+ onComplete.run();
+ mUserSwitchObservers.finishBroadcast();
+ return;
+ }
+ final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
+ synchronized (mLock) {
+ mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks;
+ }
+ final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount);
+ final long userSwitchTimeoutMs = getUserSwitchTimeoutMs();
+ final long dispatchStartedTime = SystemClock.elapsedRealtime();
+ for (int i = 0; i < observerCount; i++) {
+ final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime();
+ // Prepend with unique prefix to guarantee that keys are unique
+ final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
synchronized (mLock) {
- uss.switching = true;
- mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks;
- }
- final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount);
- final long userSwitchTimeoutMs = getUserSwitchTimeoutMs();
- final long dispatchStartedTime = SystemClock.elapsedRealtime();
- for (int i = 0; i < observerCount; i++) {
- final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime();
- try {
- // Prepend with unique prefix to guarantee that keys are unique
- final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
+ curWaitingUserSwitchCallbacks.add(name);
+ }
+ final IRemoteCallback callback = new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ asyncTraceEnd(trace + "-" + name, 0);
synchronized (mLock) {
- curWaitingUserSwitchCallbacks.add(name);
- }
- final IRemoteCallback callback = new IRemoteCallback.Stub() {
- @Override
- public void sendResult(Bundle data) throws RemoteException {
- asyncTraceEnd("onUserSwitching-" + name, newUserId);
- synchronized (mLock) {
- long delayForObserver = SystemClock.elapsedRealtime()
- - dispatchStartedTimeForObserver;
- if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) {
- Slogf.w(TAG, "User switch slowed down by observer " + name
- + ": result took " + delayForObserver
- + " ms to process.");
- }
-
- long totalDelay = SystemClock.elapsedRealtime()
- - dispatchStartedTime;
- if (totalDelay > userSwitchTimeoutMs) {
- Slogf.e(TAG, "User switch timeout: observer " + name
- + "'s result was received " + totalDelay
- + " ms after dispatchUserSwitch.");
- }
-
- curWaitingUserSwitchCallbacks.remove(name);
- // Continue switching if all callbacks have been notified and
- // user switching session is still valid
- if (waitingCallbacksCount.decrementAndGet() == 0
- && (curWaitingUserSwitchCallbacks
- == mCurWaitingUserSwitchCallbacks)) {
- sendContinueUserSwitchLU(uss, oldUserId, newUserId);
- }
- }
+ long delayForObserver = SystemClock.elapsedRealtime()
+ - dispatchStartedTimeForObserver;
+ if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) {
+ Slogf.w(TAG, "User switch slowed down by observer " + name
+ + ": result took " + delayForObserver
+ + " ms to process. " + trace);
}
- };
- asyncTraceBegin("onUserSwitching-" + name, newUserId);
- mUserSwitchObservers.getBroadcastItem(i).onUserSwitching(newUserId, callback);
- } catch (RemoteException e) {
- // Ignore
+ long totalDelay = SystemClock.elapsedRealtime() - dispatchStartedTime;
+ if (totalDelay > userSwitchTimeoutMs) {
+ Slogf.e(TAG, "User switch timeout: observer " + name
+ + "'s result was received " + totalDelay
+ + " ms after dispatchUserSwitch. " + trace);
+ }
+ curWaitingUserSwitchCallbacks.remove(name);
+ // Continue switching if all callbacks have been notified and
+ // user switching session is still valid
+ if (waitingCallbacksCount.decrementAndGet() == 0
+ && (curWaitingUserSwitchCallbacks
+ == mCurWaitingUserSwitchCallbacks)) {
+ onComplete.run();
+ }
+ }
}
- }
- } else {
- synchronized (mLock) {
- sendContinueUserSwitchLU(uss, oldUserId, newUserId);
- }
+ };
+ asyncTraceBegin(trace + "-" + name, 0);
+ consumer.accept(mUserSwitchObservers.getBroadcastItem(i), callback);
}
mUserSwitchObservers.finishBroadcast();
- t.traceEnd(); // end dispatchUserSwitch-
}
@GuardedBy("mLock")
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0fd47169122b..bd2714211796 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -242,6 +242,7 @@ import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.SystemPropertySetter;
import android.view.Display;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityManager;
@@ -11032,7 +11033,7 @@ public class AudioService extends IAudioService.Stub
@GuardedBy("mLock")
private void updateLocked() {
String n = Long.toString(mToken++);
- SystemProperties.set(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n);
+ SystemPropertySetter.setWithRetry(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n);
}
private void trigger() {
diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java
index e89f43bd7196..20c33275b8f1 100644
--- a/services/core/java/com/android/server/compat/CompatConfig.java
+++ b/services/core/java/com/android/server/compat/CompatConfig.java
@@ -876,7 +876,28 @@ final class CompatConfig {
}
@Nullable
+ @android.ravenwood.annotation.RavenwoodReplace(
+ blockedBy = PackageManager.class,
+ reason = "PackageManager.getApplicationInfo() isn't supported yet")
private Long getVersionCodeOrNull(String packageName) {
+ return getVersionCodeOrNullImpl(packageName);
+ }
+
+ @SuppressWarnings("unused")
+ @Nullable
+ private Long getVersionCodeOrNull$ravenwood(String packageName) {
+ try {
+ // It's possible that the context is mocked, try the real method first
+ return getVersionCodeOrNullImpl(packageName);
+ } catch (Throwable e) {
+ // For now, Ravenwood doesn't support the concept of "app updates", so let's
+ // just use a fixed version code for all packages.
+ return 1L;
+ }
+ }
+
+ @Nullable
+ private Long getVersionCodeOrNullImpl(String packageName) {
try {
ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(
packageName, MATCH_ANY_USER);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index c384b5434bce..9349ea54e4ee 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1030,10 +1030,20 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice {
}
@ServiceThreadOnly
+ void addAndStartAction(final HdmiCecFeatureAction action, final boolean remove) {
+ assertRunOnServiceThread();
+ if (hasAction(action.getClass()) && remove) {
+ // If the action is currently running, remove it and restart it.
+ Slog.i(TAG, action.getClass().getName() + " is in progress. Restarting.");
+ removeAction(action.getClass());
+ }
+ addAndStartAction(action);
+ }
+
+ @ServiceThreadOnly
void startNewAvbAudioStatusAction(int targetAddress) {
assertRunOnServiceThread();
- removeAction(AbsoluteVolumeAudioStatusAction.class);
- addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
+ addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress), true);
}
@ServiceThreadOnly
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index 1e90ab279d32..510e4f5e1868 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -317,11 +317,7 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
|| ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
&& lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) {
- if (hasAction(SystemAudioInitiationActionFromAvr.class)) {
- Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting.");
- removeAction(SystemAudioInitiationActionFromAvr.class);
- }
- addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+ addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
}
}
@@ -457,6 +453,7 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
HdmiLogger.debug("AVR device is not directly connected with TV");
return Constants.ABORT_NOT_IN_CORRECT_MODE;
} else {
+ // Action has been removed if it existed, do not attempt to remove again before start.
addAndStartAction(new ArcInitiationActionFromAvr(this));
return Constants.HANDLED;
}
@@ -477,11 +474,9 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
&& !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) {
IHdmiControlCallback callback =
getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0);
- removeAction(ArcTerminationActionFromAvr.class);
- addAndStartAction(new ArcTerminationActionFromAvr(this, callback));
+ addAndStartAction(new ArcTerminationActionFromAvr(this, callback), true);
} else {
- removeAction(ArcTerminationActionFromAvr.class);
- addAndStartAction(new ArcTerminationActionFromAvr(this));
+ addAndStartAction(new ArcTerminationActionFromAvr(this), true);
}
return Constants.HANDLED;
}
@@ -1036,11 +1031,7 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
void onSystemAudioControlFeatureSupportChanged(boolean enabled) {
setSystemAudioControlFeatureEnabled(enabled);
if (enabled) {
- if (hasAction(SystemAudioInitiationActionFromAvr.class)) {
- Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting.");
- removeAction(SystemAudioInitiationActionFromAvr.class);
- }
- addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+ addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
}
}
@@ -1221,8 +1212,7 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
removeAction(ArcTerminationActionFromAvr.class);
if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
&& isDirectConnectToTv() && !isArcEnabled()) {
- removeAction(ArcInitiationActionFromAvr.class);
- addAndStartAction(new ArcInitiationActionFromAvr(this));
+ addAndStartAction(new ArcInitiationActionFromAvr(this), true);
}
}
@@ -1367,10 +1357,6 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
if (mService.isDeviceDiscoveryHandledByPlayback()) {
return;
}
- if (hasAction(DeviceDiscoveryAction.class)) {
- Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
- removeAction(DeviceDiscoveryAction.class);
- }
DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
new DeviceDiscoveryCallback() {
@Override
@@ -1380,7 +1366,7 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource {
}
}
});
- addAndStartAction(action);
+ addAndStartAction(action, true);
}
@Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
index 0b667fc10880..86abbc403e50 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@@ -21,7 +21,6 @@ import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.hardware.display.DeviceProductInfo;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
@@ -32,7 +31,6 @@ import android.os.PowerManager;
import android.os.SystemProperties;
import android.sysprop.HdmiProperties;
import android.util.Slog;
-import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.LocalePicker;
@@ -151,10 +149,6 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
private void launchDeviceDiscovery() {
assertRunOnServiceThread();
clearDeviceInfoList();
- if (hasAction(DeviceDiscoveryAction.class)) {
- Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
- removeAction(DeviceDiscoveryAction.class);
- }
DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
new DeviceDiscoveryAction.DeviceDiscoveryCallback() {
@Override
@@ -163,25 +157,21 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
mService.getHdmiCecNetwork().addCecDevice(info);
}
- // Since we removed all devices when it starts and device discovery action
- // does not poll local devices, we should put device info of local device
- // manually here.
+ // Since we removed all devices when it starts and device discovery
+ // action does not poll local devices, we should put device info of
+ // local device manually here.
for (HdmiCecLocalDevice device : mService.getAllCecLocalDevices()) {
mService.getHdmiCecNetwork().addCecDevice(device.getDeviceInfo());
}
- List<HotplugDetectionAction> hotplugActions =
- getActions(HotplugDetectionAction.class);
- if (hotplugActions.isEmpty()) {
+ if (!hasAction(HotplugDetectionAction.class)) {
addAndStartAction(
- new HotplugDetectionAction(HdmiCecLocalDevicePlayback.this));
+ new HotplugDetectionAction(
+ HdmiCecLocalDevicePlayback.this));
}
if (mService.isHdmiControlEnhancedBehaviorFlagEnabled()) {
- List<PowerStatusMonitorActionFromPlayback>
- powerStatusMonitorActionsFromPlayback =
- getActions(PowerStatusMonitorActionFromPlayback.class);
- if (powerStatusMonitorActionsFromPlayback.isEmpty()) {
+ if (!hasAction(PowerStatusMonitorActionFromPlayback.class)) {
addAndStartAction(
new PowerStatusMonitorActionFromPlayback(
HdmiCecLocalDevicePlayback.this));
@@ -189,7 +179,7 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
}
}
});
- addAndStartAction(action);
+ addAndStartAction(action, true);
}
@Override
@@ -235,8 +225,16 @@ public class HdmiCecLocalDevicePlayback extends HdmiCecLocalDeviceSource {
invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
return;
}
- removeAction(DeviceSelectActionFromPlayback.class);
- addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback));
+ List<DeviceSelectActionFromPlayback> actions = getActions(
+ DeviceSelectActionFromPlayback.class);
+ if (!actions.isEmpty()) {
+ DeviceSelectActionFromPlayback action = actions.get(0);
+ if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
+ return;
+ }
+ }
+ addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback),
+ true);
}
@Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 424102cbdd89..3d6d34bf9911 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -220,10 +220,6 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer
.getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE);
if (bufferedActiveSource.isEmpty()) {
- if (hasAction(RequestActiveSourceAction.class)) {
- Slog.i(TAG, "RequestActiveSourceAction is in progress. Restarting.");
- removeAction(RequestActiveSourceAction.class);
- }
addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
@Override
public void onComplete(int result) {
@@ -231,7 +227,7 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
launchRoutingControl(routingForBootup);
}
}
- }));
+ }), true);
} else {
addCecDeviceForBufferedActiveSource(bufferedActiveSource.get(0));
}
@@ -328,8 +324,15 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
return;
}
- removeAction(DeviceSelectActionFromTv.class);
- addAndStartAction(new DeviceSelectActionFromTv(this, targetDevice, callback));
+ List<DeviceSelectActionFromTv> actions = getActions(DeviceSelectActionFromTv.class);
+ if (!actions.isEmpty()) {
+ DeviceSelectActionFromTv action = actions.get(0);
+ if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
+ return;
+ }
+ }
+ addAndStartAction(new DeviceSelectActionFromTv(this, targetDevice, callback),
+ true);
}
@ServiceThreadOnly
@@ -475,9 +478,8 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
HdmiCecMessageBuilder.buildRoutingChange(
getDeviceInfo().getLogicalAddress(), oldPath, newPath);
mService.sendCecCommand(routingChange);
- removeAction(RoutingControlAction.class);
addAndStartAction(
- new RoutingControlAction(this, newPath, callback));
+ new RoutingControlAction(this, newPath, callback), true);
}
@ServiceThreadOnly
@@ -801,16 +803,12 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
mSelectRequestBuffer.process();
resetSelectRequestBuffer();
- List<HotplugDetectionAction> hotplugActions
- = getActions(HotplugDetectionAction.class);
- if (hotplugActions.isEmpty()) {
+ if (!hasAction(HotplugDetectionAction.class)) {
addAndStartAction(
new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
}
- List<PowerStatusMonitorAction> powerStatusActions
- = getActions(PowerStatusMonitorAction.class);
- if (powerStatusActions.isEmpty()) {
+ if (!hasAction(PowerStatusMonitorAction.class)) {
addAndStartAction(
new PowerStatusMonitorAction(HdmiCecLocalDeviceTv.this));
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 35ef18b144e7..bd8b67b9d626 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -17,7 +17,6 @@
package com.android.server.hdmi;
import static android.media.tv.flags.Flags.hdmiControlEnhancedBehavior;
-
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_DISABLED;
@@ -107,7 +106,6 @@ import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.KeyEvent;
-import android.view.WindowManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -1218,9 +1216,6 @@ public class HdmiControlService extends SystemService {
audioSystem.terminateSystemAudioMode();
}
if (isArcEnabled) {
- if (audioSystem.hasAction(ArcTerminationActionFromAvr.class)) {
- audioSystem.removeAction(ArcTerminationActionFromAvr.class);
- }
audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem,
new IHdmiControlCallback.Stub() {
@Override
@@ -1228,7 +1223,7 @@ public class HdmiControlService extends SystemService {
mAddressAllocated = false;
initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
}
- }));
+ }), true);
}
}
if (!isArcEnabled) {
diff --git a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
index 9a3cde156300..d05ded5367d0 100644
--- a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
+++ b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
@@ -68,6 +68,8 @@ public class PowerStatusMonitorActionFromPlayback extends HdmiCecFeatureAction {
private boolean handleReportPowerStatusFromTv(HdmiCecMessage cmd) {
int powerStatus = cmd.getParams()[0] & 0xFF;
+ mState = STATE_WAIT_FOR_NEXT_MONITORING;
+ addTimer(mState, MONITORING_INTERVAL_MS);
if (powerStatus == POWER_STATUS_STANDBY) {
Slog.d(TAG, "TV reported it turned off, going to sleep.");
source().getService().standby();
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
index 7698a87f80c9..740c4f195852 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
@@ -36,6 +36,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
/**
* A class that manages registration/unregistration of clients and manages messages to/from clients.
@@ -312,15 +313,9 @@ import java.util.concurrent.ConcurrentHashMap;
@Override
public void onCloseEndpointSession(int sessionId, byte reason) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onCloseEndpointSession(sessionId, reason);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId, (broker) -> broker.onCloseEndpointSession(sessionId, reason));
if (!callbackInvoked) {
Log.w(TAG, "onCloseEndpointSession: unknown session ID " + sessionId);
}
@@ -328,15 +323,9 @@ import java.util.concurrent.ConcurrentHashMap;
@Override
public void onEndpointSessionOpenComplete(int sessionId) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onEndpointSessionOpenComplete(sessionId);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId, (broker) -> broker.onEndpointSessionOpenComplete(sessionId));
if (!callbackInvoked) {
Log.w(TAG, "onEndpointSessionOpenComplete: unknown session ID " + sessionId);
}
@@ -344,15 +333,9 @@ import java.util.concurrent.ConcurrentHashMap;
@Override
public void onMessageReceived(int sessionId, HubMessage message) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onMessageReceived(sessionId, message);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId, (broker) -> broker.onMessageReceived(sessionId, message));
if (!callbackInvoked) {
Log.w(TAG, "onMessageReceived: unknown session ID " + sessionId);
}
@@ -360,18 +343,36 @@ import java.util.concurrent.ConcurrentHashMap;
@Override
public void onMessageDeliveryStatusReceived(int sessionId, int sequenceNumber, byte errorCode) {
- boolean callbackInvoked = false;
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId,
+ (broker) ->
+ broker.onMessageDeliveryStatusReceived(
+ sessionId, sequenceNumber, errorCode));
+ if (!callbackInvoked) {
+ Log.w(TAG, "onMessageDeliveryStatusReceived: unknown session ID " + sessionId);
+ }
+ }
+
+ /**
+ * Invokes a callback for a session with matching ID.
+ *
+ * @param callback The callback to execute
+ * @return true if a callback was executed
+ */
+ private boolean invokeCallbackForMatchingSession(
+ int sessionId, Consumer<ContextHubEndpointBroker> callback) {
for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
if (broker.hasSessionId(sessionId)) {
- broker.onMessageDeliveryStatusReceived(sessionId, sequenceNumber, errorCode);
- callbackInvoked = true;
- break;
+ try {
+ callback.accept(broker);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Exception while invoking callback", e);
+ }
+ return true;
}
}
-
- if (!callbackInvoked) {
- Log.w(TAG, "onMessageDeliveryStatusReceived: unknown session ID " + sessionId);
- }
+ return false;
}
/** Unregister the hub (called during init() failure). Silence errors. */
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 54ed5a9711b3..2615a76ac279 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -756,9 +756,7 @@ public class ContextHubService extends IContextHubService.Stub {
@Override
public List<HubInfo> getHubs() throws RemoteException {
super.getHubs_enforcePermission();
- if (mHubInfoRegistry == null) {
- return Collections.emptyList();
- }
+ checkHubDiscoveryPreconditions();
return mHubInfoRegistry.getHubs();
}
@@ -766,9 +764,7 @@ public class ContextHubService extends IContextHubService.Stub {
@Override
public List<HubEndpointInfo> findEndpoints(long endpointId) {
super.findEndpoints_enforcePermission();
- if (mHubInfoRegistry == null) {
- return Collections.emptyList();
- }
+ checkEndpointDiscoveryPreconditions();
return mHubInfoRegistry.findEndpoints(endpointId);
}
@@ -776,9 +772,7 @@ public class ContextHubService extends IContextHubService.Stub {
@Override
public List<HubEndpointInfo> findEndpointsWithService(String serviceDescriptor) {
super.findEndpointsWithService_enforcePermission();
- if (mHubInfoRegistry == null) {
- return Collections.emptyList();
- }
+ checkEndpointDiscoveryPreconditions();
return mHubInfoRegistry.findEndpointsWithService(serviceDescriptor);
}
@@ -834,6 +828,13 @@ public class ContextHubService extends IContextHubService.Stub {
}
}
+ private void checkHubDiscoveryPreconditions() {
+ if (mHubInfoRegistry == null) {
+ Log.e(TAG, "Hub registry failed to initialize");
+ throw new UnsupportedOperationException("Hub discovery is not supported");
+ }
+ }
+
/**
* Creates an internal load transaction callback to be used for old API clients
*
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 3f915757f137..286238e7888c 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -451,6 +451,7 @@ public class LockSettingsService extends ILockSettings.Stub {
* @param profileUserId profile user Id
* @param profileUserPassword profile original password (when it has separated lock).
*/
+ @GuardedBy("mSpManager")
private void tieProfileLockIfNecessary(int profileUserId,
LockscreenCredential profileUserPassword) {
// Only for profiles that shares credential with parent
@@ -909,14 +910,8 @@ public class LockSettingsService extends ILockSettings.Stub {
// Hide notification first, as tie profile lock takes time
hideEncryptionNotification(new UserHandle(userId));
- if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
- synchronized (mSpManager) {
- tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
- }
- } else {
- if (isCredentialSharableWithParent(userId)) {
- tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
- }
+ synchronized (mSpManager) {
+ tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
}
}
});
@@ -1380,11 +1375,7 @@ public class LockSettingsService extends ILockSettings.Stub {
mStorage.removeChildProfileLock(userId);
removeKeystoreProfileKey(userId);
} else {
- if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
- synchronized (mSpManager) {
- tieProfileLockIfNecessary(userId, profileUserPassword);
- }
- } else {
+ synchronized (mSpManager) {
tieProfileLockIfNecessary(userId, profileUserPassword);
}
}
diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
index 38f39393a025..cc5c88b77293 100644
--- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java
+++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
@@ -50,6 +50,10 @@ public class OverlayActorEnforcer {
*/
static Pair<String, ActorState> getPackageNameForActor(@NonNull String actorUriString,
@NonNull Map<String, Map<String, String>> namedActors) {
+ if (namedActors.isEmpty()) {
+ return Pair.create(null, ActorState.NO_NAMED_ACTORS);
+ }
+
Uri actorUri = Uri.parse(actorUriString);
String actorScheme = actorUri.getScheme();
@@ -58,10 +62,6 @@ public class OverlayActorEnforcer {
return Pair.create(null, ActorState.INVALID_OVERLAYABLE_ACTOR_NAME);
}
- if (namedActors.isEmpty()) {
- return Pair.create(null, ActorState.NO_NAMED_ACTORS);
- }
-
String actorNamespace = actorUri.getAuthority();
Map<String, String> namespace = namedActors.get(actorNamespace);
if (ArrayUtils.isEmpty(namespace)) {
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 1726f0da9cbe..a6f2a3757dcb 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -121,6 +121,8 @@ public final class HintManagerService extends SystemService {
@VisibleForTesting final long mHintSessionPreferredRate;
@VisibleForTesting static final int MAX_GRAPHICS_PIPELINE_THREADS_COUNT = 5;
+ private static final int DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT = 5;
+ private static final int DEFAULT_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS = 50;
// Multi-level map storing all active AppHintSessions.
// First level is keyed by the UID of the client process creating the session.
@@ -206,12 +208,17 @@ public final class HintManagerService extends SystemService {
"persist.hms.check_headroom_affinity";
private static final String PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS =
"persist.hms.check_headroom_proc_stat_min_millis";
+ private static final String PROPERTY_CPU_HEADROOM_TID_MAX_CNT =
+ "persist.hms.cpu_headroom_tid_max_cnt";
private Boolean mFMQUsesIntegratedEventFlag = false;
private final Object mCpuHeadroomLock = new Object();
@VisibleForTesting
final float mJiffyMillis;
+ private final boolean mCheckHeadroomTid;
+ private final boolean mCheckHeadroomAffinity;
private final int mCheckHeadroomProcStatMinMillis;
+ private final int mCpuHeadroomMaxTidCnt;
@GuardedBy("mCpuHeadroomLock")
private long mLastCpuUserModeTimeCheckedMillis = 0;
@GuardedBy("mCpuHeadroomLock")
@@ -339,13 +346,23 @@ public final class HintManagerService extends SystemService {
mUidToLastUserModeJiffies = new ArrayMap<>();
long jiffyHz = Os.sysconf(OsConstants._SC_CLK_TCK);
mJiffyMillis = 1000.0f / jiffyHz;
+ mCheckHeadroomTid = SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_TID, true);
+ mCheckHeadroomAffinity = SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_AFFINITY,
+ true);
mCheckHeadroomProcStatMinMillis = SystemProperties.getInt(
- PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS, 50);
+ PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS,
+ DEFAULT_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS);
+ mCpuHeadroomMaxTidCnt = Math.min(SystemProperties.getInt(
+ PROPERTY_CPU_HEADROOM_TID_MAX_CNT, DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT),
+ mSupportInfo.headroom.cpuMaxTidCount);
} else {
mCpuHeadroomCache = null;
mUidToLastUserModeJiffies = null;
mJiffyMillis = 0.0f;
+ mCheckHeadroomTid = true;
+ mCheckHeadroomAffinity = true;
mCheckHeadroomProcStatMinMillis = 0;
+ mCpuHeadroomMaxTidCnt = 0;
}
if (mSupportInfo.headroom.isGpuSupported) {
mGpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.gpuMinIntervalMillis);
@@ -1577,8 +1594,7 @@ public final class HintManagerService extends SystemService {
if (params.usesDeviceHeadroom) {
halParams.tids = new int[]{};
} else if (params.tids != null && params.tids.length > 0) {
- if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && SystemProperties.getBoolean(
- PROPERTY_CHECK_HEADROOM_TID, true)) {
+ if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && mCheckHeadroomTid) {
final int tgid = Process.getThreadGroupLeader(Binder.getCallingPid());
for (int tid : params.tids) {
if (Process.getThreadGroupLeader(tid) != tgid) {
@@ -1588,8 +1604,8 @@ public final class HintManagerService extends SystemService {
}
}
}
- if (cpuHeadroomAffinityCheck() && params.tids.length > 1
- && SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_AFFINITY, true)) {
+ if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity
+ && params.tids.length > 1) {
checkThreadAffinityForTids(params.tids);
}
halParams.tids = params.tids;
@@ -1709,15 +1725,22 @@ public final class HintManagerService extends SystemService {
throw new IllegalArgumentException(
"Unknown CPU headroom calculation type " + (int) params.calculationType);
}
- if (params.calculationWindowMillis < 50 || params.calculationWindowMillis > 10000) {
+ if (params.calculationWindowMillis < mSupportInfo.headroom.cpuMinCalculationWindowMillis
+ || params.calculationWindowMillis
+ > mSupportInfo.headroom.cpuMaxCalculationWindowMillis) {
throw new IllegalArgumentException(
- "Invalid CPU headroom calculation window, expected [50, 10000] but got "
+ "Invalid CPU headroom calculation window, expected ["
+ + mSupportInfo.headroom.cpuMinCalculationWindowMillis
+ + ", "
+ + mSupportInfo.headroom.cpuMaxCalculationWindowMillis
+ + "] but got "
+ params.calculationWindowMillis);
}
if (!params.usesDeviceHeadroom) {
- if (params.tids != null && params.tids.length > 5) {
+ if (params.tids != null && params.tids.length > mCpuHeadroomMaxTidCnt) {
throw new IllegalArgumentException(
- "More than 5 TIDs requested: " + params.tids.length);
+ "More than " + mCpuHeadroomMaxTidCnt + " TIDs requested: "
+ + params.tids.length);
}
}
}
@@ -1772,9 +1795,13 @@ public final class HintManagerService extends SystemService {
throw new IllegalArgumentException(
"Unknown GPU headroom calculation type " + (int) params.calculationType);
}
- if (params.calculationWindowMillis < 50 || params.calculationWindowMillis > 10000) {
+ if (params.calculationWindowMillis < mSupportInfo.headroom.gpuMinCalculationWindowMillis
+ || params.calculationWindowMillis
+ > mSupportInfo.headroom.gpuMaxCalculationWindowMillis) {
throw new IllegalArgumentException(
- "Invalid GPU headroom calculation window, expected [50, 10000] but got "
+ "Invalid GPU headroom calculation window, expected ["
+ + mSupportInfo.headroom.gpuMinCalculationWindowMillis + ", "
+ + mSupportInfo.headroom.gpuMaxCalculationWindowMillis + "] but got "
+ params.calculationWindowMillis);
}
}
@@ -1807,9 +1834,15 @@ public final class HintManagerService extends SystemService {
@Override
public IHintManager.HintManagerClientData
registerClient(@NonNull IHintManager.IHintManagerClient clientBinder) {
+ return getClientData();
+ }
+
+ @Override
+ public IHintManager.HintManagerClientData getClientData() {
IHintManager.HintManagerClientData out = new IHintManager.HintManagerClientData();
out.preferredRateNanos = mHintSessionPreferredRate;
out.maxGraphicsPipelineThreads = getMaxGraphicsPipelineThreadsCount();
+ out.maxCpuHeadroomThreads = DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT;
out.powerHalVersion = mPowerHalVersion;
out.supportInfo = mSupportInfo;
return out;
@@ -1838,23 +1871,40 @@ public final class HintManagerService extends SystemService {
}
}
}
- pw.println("CPU Headroom Interval: " + mSupportInfo.headroom.cpuMinIntervalMillis);
- pw.println("GPU Headroom Interval: " + mSupportInfo.headroom.gpuMinIntervalMillis);
- try {
- CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
- params.usesDeviceHeadroom = true;
- CpuHeadroomResult ret = getCpuHeadroom(params);
- pw.println("CPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
- } catch (Exception e) {
- Slog.d(TAG, "Failed to dump CPU headroom", e);
- pw.println("CPU headroom: N/A");
+ pw.println("CPU Headroom Supported: " + mSupportInfo.headroom.isCpuSupported);
+ if (mSupportInfo.headroom.isCpuSupported) {
+ pw.println("CPU Headroom Interval: " + mSupportInfo.headroom.cpuMinIntervalMillis);
+ pw.println("CPU Headroom TID Max Count: " + mCpuHeadroomMaxTidCnt);
+ pw.println("CPU Headroom TID Max Count From HAL: "
+ + mSupportInfo.headroom.cpuMaxTidCount);
+ pw.println("CPU Headroom Calculation Window Range: ["
+ + mSupportInfo.headroom.cpuMinCalculationWindowMillis + ", "
+ + mSupportInfo.headroom.cpuMaxCalculationWindowMillis + "]");
+ try {
+ CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
+ params.usesDeviceHeadroom = true;
+ CpuHeadroomResult ret = getCpuHeadroom(params);
+ pw.println("CPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
+ } catch (Exception e) {
+ Slog.d(TAG, "Failed to dump CPU headroom", e);
+ pw.println("CPU headroom: N/A");
+ }
}
- try {
- GpuHeadroomResult ret = getGpuHeadroom(new GpuHeadroomParamsInternal());
- pw.println("GPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
- } catch (Exception e) {
- Slog.d(TAG, "Failed to dump GPU headroom", e);
- pw.println("GPU headroom: N/A");
+ pw.println("GPU Headroom Supported: " + mSupportInfo.headroom.isGpuSupported);
+ if (mSupportInfo.headroom.isGpuSupported) {
+ pw.println("GPU Headroom Interval: " + mSupportInfo.headroom.gpuMinIntervalMillis);
+ pw.println("GPU Headroom Calculation Window Range: ["
+ + mSupportInfo.headroom.gpuMinCalculationWindowMillis + ", "
+ + mSupportInfo.headroom.gpuMaxCalculationWindowMillis + "]");
+ try {
+ GpuHeadroomParamsInternal params = new GpuHeadroomParamsInternal();
+ params.calculationWindowMillis = mDefaultGpuHeadroomCalculationWindowMillis;
+ GpuHeadroomResult ret = getGpuHeadroom(params);
+ pw.println("GPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
+ } catch (Exception e) {
+ Slog.d(TAG, "Failed to dump GPU headroom", e);
+ pw.println("GPU headroom: N/A");
+ }
}
}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index caaf5a2b16d0..9206cce12cd6 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -2205,6 +2205,11 @@ public class BatteryStatsImpl extends BatteryStats {
getWakelockDurationRetriever() {
return mWakelockDurationRetriever;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return BatteryStatsImpl.this.networkStatsDelta(stats, oldStats);
+ }
}
private final PowerStatsCollectorInjector mPowerStatsCollectorInjector =
@@ -12392,83 +12397,13 @@ public class BatteryStatsImpl extends BatteryStats {
return networkStatsManager.getWifiUidStats();
}
- static class NetworkStatsDelta {
- int mUid;
- int mSet;
- long mRxBytes;
- long mRxPackets;
- long mTxBytes;
- long mTxPackets;
-
- public int getUid() {
- return mUid;
- }
-
-
- public int getSet() {
- return mSet;
- }
-
- public long getRxBytes() {
- return mRxBytes;
- }
-
- public long getRxPackets() {
- return mRxPackets;
- }
-
- public long getTxBytes() {
- return mTxBytes;
- }
-
- public long getTxPackets() {
- return mTxPackets;
- }
-
- @Override
- public String toString() {
- return "NetworkStatsDelta{mUid=" + mUid + ", mSet=" + mSet + ", mRxBytes=" + mRxBytes
- + ", mRxPackets=" + mRxPackets + ", mTxBytes=" + mTxBytes + ", mTxPackets="
- + mTxPackets + '}';
- }
- }
-
- static List<NetworkStatsDelta> computeDelta(NetworkStats currentStats,
- NetworkStats lastStats) {
- List<NetworkStatsDelta> deltaList = new ArrayList<>();
- for (NetworkStats.Entry entry : currentStats) {
- NetworkStatsDelta delta = new NetworkStatsDelta();
- delta.mUid = entry.getUid();
- delta.mSet = entry.getSet();
- NetworkStats.Entry lastEntry = null;
- if (lastStats != null) {
- for (NetworkStats.Entry e : lastStats) {
- if (e.getUid() == entry.getUid() && e.getSet() == entry.getSet()
- && e.getTag() == entry.getTag()
- && e.getMetered() == entry.getMetered()
- && e.getRoaming() == entry.getRoaming()
- && e.getDefaultNetwork() == entry.getDefaultNetwork()
- /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
- lastEntry = e;
- break;
- }
- }
- }
- if (lastEntry != null) {
- delta.mRxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes());
- delta.mRxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets());
- delta.mTxBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes());
- delta.mTxPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets());
- } else {
- delta.mRxBytes = entry.getRxBytes();
- delta.mRxPackets = entry.getRxPackets();
- delta.mTxBytes = entry.getTxBytes();
- delta.mTxPackets = entry.getTxPackets();
- }
- deltaList.add(delta);
+ @VisibleForTesting
+ protected NetworkStats networkStatsDelta(@NonNull NetworkStats stats,
+ @Nullable NetworkStats oldStats) {
+ if (oldStats == null) {
+ return stats;
}
-
- return deltaList;
+ return stats.subtract(oldStats);
}
/**
@@ -12486,12 +12421,12 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
+ NetworkStats delta;
// Grab a separate lock to acquire the network stats, which may do I/O.
- List<NetworkStatsDelta> delta;
synchronized (mWifiNetworkLock) {
final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager);
if (latestStats != null) {
- delta = computeDelta(latestStats, mLastWifiNetworkStats);
+ delta = networkStatsDelta(latestStats, mLastWifiNetworkStats);
mLastWifiNetworkStats = latestStats;
} else {
delta = null;
@@ -12501,15 +12436,15 @@ public class BatteryStatsImpl extends BatteryStats {
}
private void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo wifiActivityEnergyInfo,
- List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+ NetworkStats networkStatsDelta, long elapsedRealtimeMs, long uptimeMs) {
// Do not populate consumed energy, because energy attribution is done by
// WifiPowerStatsProcessor.
- updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+ updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDelta, POWER_DATA_UNAVAILABLE,
elapsedRealtimeMs, uptimeMs);
}
private void updateWifiBatteryStats(WifiActivityEnergyInfo info,
- List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+ NetworkStats delta, long consumedChargeUC, long elapsedRealtimeMs,
long uptimeMs) {
synchronized (this) {
if (!mOnBatteryInternal || mIgnoreNextExternalStats) {
@@ -12535,7 +12470,7 @@ public class BatteryStatsImpl extends BatteryStats {
long totalTxPackets = 0;
long totalRxPackets = 0;
if (delta != null) {
- for (NetworkStatsDelta entry : delta) {
+ for (NetworkStats.Entry entry : delta) {
if (DEBUG_ENERGY) {
Slog.d(TAG, "Wifi uid " + entry.getUid()
+ ": delta rx=" + entry.getRxBytes()
@@ -12879,11 +12814,11 @@ public class BatteryStatsImpl extends BatteryStats {
mLastModemActivityInfo = activityInfo;
// Grab a separate lock to acquire the network stats, which may do I/O.
- List<NetworkStatsDelta> delta = null;
+ NetworkStats delta = null;
synchronized (mModemNetworkLock) {
final NetworkStats latestStats = readMobileNetworkStatsLocked(networkStatsManager);
if (latestStats != null) {
- delta = computeDelta(latestStats, mLastModemNetworkStats);
+ delta = networkStatsDelta(latestStats, mLastModemNetworkStats);
mLastModemNetworkStats = latestStats;
}
}
@@ -12892,15 +12827,15 @@ public class BatteryStatsImpl extends BatteryStats {
}
private void onMobileRadioPowerStatsRetrieved(ModemActivityInfo modemActivityInfo,
- List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+ NetworkStats networkStatsDelta, long elapsedRealtimeMs, long uptimeMs) {
// Do not populate consumed energy, because energy attribution is done by
// MobileRadioPowerStatsProcessor.
- updateCellularBatteryStats(modemActivityInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+ updateCellularBatteryStats(modemActivityInfo, networkStatsDelta, POWER_DATA_UNAVAILABLE,
elapsedRealtimeMs, uptimeMs);
}
private void updateCellularBatteryStats(@Nullable ModemActivityInfo deltaInfo,
- @Nullable List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+ @Nullable NetworkStats delta, long consumedChargeUC, long elapsedRealtimeMs,
long uptimeMs) {
// Add modem tx power to history.
addModemTxPowerToHistory(deltaInfo, elapsedRealtimeMs, uptimeMs);
@@ -13003,7 +12938,7 @@ public class BatteryStatsImpl extends BatteryStats {
long totalRxPackets = 0;
long totalTxPackets = 0;
if (delta != null) {
- for (NetworkStatsDelta entry : delta) {
+ for (NetworkStats.Entry entry : delta) {
if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
continue;
}
@@ -13044,7 +12979,7 @@ public class BatteryStatsImpl extends BatteryStats {
// Now distribute proportional blame to the apps that did networking.
long totalPackets = totalRxPackets + totalTxPackets;
if (totalPackets > 0) {
- for (NetworkStatsDelta entry : delta) {
+ for (NetworkStats.Entry entry : delta) {
if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
continue;
}
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
index cbd6fab2a9f7..f971e2e882c3 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
@@ -38,7 +38,6 @@ import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
@@ -71,7 +70,7 @@ public class MobileRadioPowerStatsCollector extends PowerStatsCollector {
interface Observer {
void onMobileRadioPowerStatsRetrieved(
@Nullable ModemActivityInfo modemActivityDelta,
- @Nullable List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas,
+ @Nullable NetworkStats networkStatsDeltas,
long elapsedRealtimeMs, long uptimeMs);
}
@@ -86,6 +85,8 @@ public class MobileRadioPowerStatsCollector extends PowerStatsCollector {
TelephonyManager getTelephonyManager();
LongSupplier getCallDurationSupplier();
LongSupplier getPhoneSignalScanDurationSupplier();
+
+ NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats);
}
private final Injector mInjector;
@@ -190,7 +191,7 @@ public class MobileRadioPowerStatsCollector extends PowerStatsCollector {
mPowerStats.uidStats.clear();
ModemActivityInfo modemActivityDelta = collectModemActivityInfo();
- List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
+ NetworkStats networkStatsDeltas = collectNetworkStats();
mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
@@ -288,17 +289,15 @@ public class MobileRadioPowerStatsCollector extends PowerStatsCollector {
return deltaInfo;
}
- private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
+ private NetworkStats collectNetworkStats() {
NetworkStats networkStats = mNetworkStatsSupplier.get();
if (networkStats == null) {
return null;
}
- List<BatteryStatsImpl.NetworkStatsDelta> delta =
- BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats);
+ NetworkStats delta = mInjector.networkStatsDelta(networkStats, mLastNetworkStats);
mLastNetworkStats = networkStats;
- for (int i = delta.size() - 1; i >= 0; i--) {
- BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i);
+ for (NetworkStats.Entry uidDelta : delta) {
long rxBytes = uidDelta.getRxBytes();
long txBytes = uidDelta.getTxBytes();
long rxPackets = uidDelta.getRxPackets();
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
index 1fdeac9816d0..5440bcf1d124 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -31,7 +31,6 @@ import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.WifiPowerStatsLayout;
import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@@ -43,7 +42,7 @@ public class WifiPowerStatsCollector extends PowerStatsCollector {
interface Observer {
void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo info,
- List<BatteryStatsImpl.NetworkStatsDelta> delta, long elapsedRealtimeMs,
+ NetworkStats delta, long elapsedRealtimeMs,
long uptimeMs);
}
@@ -66,6 +65,8 @@ public class WifiPowerStatsCollector extends PowerStatsCollector {
Supplier<NetworkStats> getWifiNetworkStatsSupplier();
WifiManager getWifiManager();
WifiStatsRetriever getWifiStatsRetriever();
+
+ NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats);
}
private final Injector mInjector;
@@ -161,7 +162,7 @@ public class WifiPowerStatsCollector extends PowerStatsCollector {
} else {
collectWifiActivityStats();
}
- List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
+ NetworkStats networkStatsDeltas = collectNetworkStats();
collectWifiScanTime();
mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
@@ -227,17 +228,15 @@ public class WifiPowerStatsCollector extends PowerStatsCollector {
mPowerStats.durationMs = duration;
}
- private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
+ private NetworkStats collectNetworkStats() {
NetworkStats networkStats = mNetworkStatsSupplier.get();
if (networkStats == null) {
return null;
}
- List<BatteryStatsImpl.NetworkStatsDelta> delta =
- BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats);
+ NetworkStats delta = mInjector.networkStatsDelta(networkStats, mLastNetworkStats);
mLastNetworkStats = networkStats;
- for (int i = delta.size() - 1; i >= 0; i--) {
- BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i);
+ for (NetworkStats.Entry uidDelta : delta) {
long rxBytes = uidDelta.getRxBytes();
long txBytes = uidDelta.getTxBytes();
long rxPackets = uidDelta.getRxPackets();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5dbdeff672e7..093df8c5075c 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -367,7 +367,6 @@ import com.android.internal.content.ReferrerIntent;
import com.android.internal.os.TimeoutRecord;
import com.android.internal.os.TransferPipe;
import com.android.internal.policy.AttributeCache;
-import com.android.internal.policy.PhoneWindow;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
@@ -2027,8 +2026,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
|| ent.array.getBoolean(R.styleable.Window_windowShowWallpaper, false);
mStyleFillsParent = mOccludesParent;
mNoDisplay = ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false);
- mOptOutEdgeToEdge = PhoneWindow.isOptingOutEdgeToEdgeEnforcement(
- aInfo.applicationInfo, false /* local */, ent.array);
+ mOptOutEdgeToEdge = ent.array.getBoolean(
+ R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false);
} else {
mStyleFillsParent = mOccludesParent = true;
mNoDisplay = false;
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 98ed6f76b2f9..54ae80cfe98a 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -103,6 +103,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
// again, so that the control with leash can be eventually dispatched
if (!mGivenInsetsReady && isServerVisible() && !givenInsetsPending
&& mControlTarget != null) {
+ ProtoLog.d(WM_DEBUG_IME,
+ "onPostLayout: IME control ready to be dispatched, ws=%s", ws);
mGivenInsetsReady = true;
ImeTracker.forLogging().onProgress(mStatsToken,
ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
@@ -118,6 +120,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
mStatsToken = null;
} else if (wasServerVisible && !isServerVisible()) {
+ ProtoLog.d(WM_DEBUG_IME, "onPostLayout: setImeShowing(false) was: %s, ws=%s",
+ isImeShowing(), ws);
setImeShowing(false);
}
}
@@ -621,6 +625,7 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
// request (cancelling the initial show) or hide request (aborting the initial show).
logIsScheduledAndReadyToShowIme(!visible /* aborted */);
}
+ ProtoLog.d(WM_DEBUG_IME, "receiveImeStatsToken: visible=%s", visible);
if (visible) {
ImeTracker.forLogging().onCancelled(
mStatsToken, ImeTracker.PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9ab9a8f44ed9..c5d42ad9f081 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1787,9 +1787,11 @@ public final class SystemServer implements Dumpable {
SignedConfigService.registerUpdateReceiver(mSystemContext);
t.traceEnd();
- t.traceBegin("AppIntegrityService");
- mSystemServiceManager.startService(AppIntegrityManagerService.class);
- t.traceEnd();
+ if (!android.server.Flags.removeAppIntegrityManagerService()) {
+ t.traceBegin("AppIntegrityService");
+ mSystemServiceManager.startService(AppIntegrityManagerService.class);
+ t.traceEnd();
+ }
t.traceBegin("StartLogcatManager");
mSystemServiceManager.startService(LogcatManagerService.class);
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 0d222fb4409e..4d021ec2c0d3 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -51,4 +51,11 @@ flag {
description: "Remove GameManagerService from Wear"
bug: "340929737"
is_fixed_read_only: true
+}
+
+flag {
+ name: "remove_app_integrity_manager_service"
+ namespace: "package_manager_service"
+ description: "Remove AppIntegrityManagerService"
+ bug: "364200023"
} \ No newline at end of file
diff --git a/services/robotests/Android.bp b/services/robotests/Android.bp
index 6c4158e60ebb..8e0eb6b14432 100644
--- a/services/robotests/Android.bp
+++ b/services/robotests/Android.bp
@@ -63,7 +63,6 @@ android_robolectric_test {
instrumentation_for: "FrameworksServicesLib",
- upstream: true,
strict_mode: false,
}
diff --git a/services/robotests/backup/Android.bp b/services/robotests/backup/Android.bp
index 3ace3fb11506..95b38e56f276 100644
--- a/services/robotests/backup/Android.bp
+++ b/services/robotests/backup/Android.bp
@@ -66,7 +66,6 @@ android_robolectric_test {
instrumentation_for: "BackupFrameworksServicesLib",
- upstream: true,
strict_mode: false,
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index e6ff5068368f..da58aa1f6c66 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -86,6 +86,7 @@ android_ravenwood_test {
"src/com/android/server/inputmethod/**/ClientControllerTest.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
android_test {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 9e96800ca2e9..4a09802fc822 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -258,7 +258,6 @@ public class MockingOomAdjusterTests {
mService.mOomAdjuster = mService.mProcessStateController.getOomAdjuster();
mService.mOomAdjuster.mAdjSeq = 10000;
mService.mWakefulness = new AtomicInteger(PowerManagerInternal.WAKEFULNESS_AWAKE);
- mSetFlagsRule.enableFlags(Flags.FLAG_NEW_FGS_RESTRICTION_LOGIC);
mUiTierSize = mService.mConstants.TIERED_CACHED_ADJ_UI_TIER_SIZE;
mFirstNonUiCachedAdj = sFirstUiCachedAdj + mUiTierSize;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
index 00b911bc1ae7..cd3683ba0ca8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
@@ -158,6 +158,11 @@ public class MobileRadioPowerStatsCollectorTest {
public LongSupplier getPhoneSignalScanDurationSupplier() {
return mScanDurationSupplier;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 4b6fcc39dcef..8a081f8e16cc 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -283,6 +283,11 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl {
protected void updateBatteryPropertiesLocked() {
}
+ @Override
+ protected NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
+
public static class DummyExternalStatsSync implements ExternalStatsSync {
public int flags = 0;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java b/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java
new file mode 100644
index 000000000000..21be6546b011
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.power.stats;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NetworkStatsTestUtils {
+ /**
+ * Equivalent to NetworkStats.subtract, reimplementing the method for Ravenwood tests.
+ */
+ @NonNull
+ public static NetworkStats networkStatsDelta(@NonNull NetworkStats currentStats,
+ @Nullable NetworkStats lastStats) {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ if (lastStats == null) {
+ return currentStats;
+ }
+ return currentStats.subtract(lastStats);
+ }
+
+ List<NetworkStats.Entry> entries = new ArrayList<>();
+ for (NetworkStats.Entry entry : currentStats) {
+ NetworkStats.Entry lastEntry = null;
+ int uid = entry.getUid();
+ if (lastStats != null) {
+ for (NetworkStats.Entry e : lastStats) {
+ if (e.getUid() == uid && e.getSet() == entry.getSet()
+ && e.getTag() == entry.getTag()
+ && e.getMetered() == entry.getMetered()
+ && e.getRoaming() == entry.getRoaming()
+ && e.getDefaultNetwork() == entry.getDefaultNetwork()
+ /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
+ lastEntry = e;
+ break;
+ }
+ }
+ }
+ long rxBytes, rxPackets, txBytes, txPackets;
+ if (lastEntry != null) {
+ rxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes());
+ rxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets());
+ txBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes());
+ txPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets());
+ } else {
+ rxBytes = entry.getRxBytes();
+ rxPackets = entry.getRxPackets();
+ txBytes = entry.getTxBytes();
+ txPackets = entry.getTxPackets();
+ }
+
+ NetworkStats.Entry uidEntry = mock(NetworkStats.Entry.class);
+ when(uidEntry.getUid()).thenReturn(uid);
+ when(uidEntry.getRxBytes()).thenReturn(rxBytes);
+ when(uidEntry.getRxPackets()).thenReturn(rxPackets);
+ when(uidEntry.getTxBytes()).thenReturn(txBytes);
+ when(uidEntry.getTxPackets()).thenReturn(txPackets);
+
+ entries.add(uidEntry);
+ }
+ NetworkStats delta = mock(NetworkStats.class);
+ when(delta.iterator()).thenAnswer(inv -> entries.iterator());
+ return delta;
+ }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
index 8b5e6ee9cf89..a26b2c955380 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
@@ -168,6 +168,11 @@ public class WifiPowerStatsCollectorTest {
public WifiManager getWifiManager() {
return mWifiManager;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
index 4ed44a0563e3..6acd36885b1e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
@@ -56,6 +56,7 @@ import com.android.internal.os.Clock;
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MobileRadioPowerStatsCollector;
+import com.android.server.power.stats.NetworkStatsTestUtils;
import com.android.server.power.stats.PowerStatsCollector;
import com.android.server.power.stats.PowerStatsUidResolver;
import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
@@ -152,6 +153,11 @@ public class MobileRadioPowerStatsProcessorTest {
public LongSupplier getPhoneSignalScanDurationSupplier() {
return mScanDurationSupplier;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
index 535f2da603b8..a20274fb5ded 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
@@ -43,6 +43,7 @@ import android.telephony.TelephonyManager;
import com.android.internal.os.Clock;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MobileRadioPowerStatsCollector;
+import com.android.server.power.stats.NetworkStatsTestUtils;
import com.android.server.power.stats.PowerStatsCollector;
import com.android.server.power.stats.PowerStatsUidResolver;
import com.android.server.power.stats.format.PowerStatsLayout;
@@ -135,6 +136,11 @@ public class PhoneCallPowerStatsProcessorTest {
public LongSupplier getPhoneSignalScanDurationSupplier() {
return mScanDurationSupplier;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
index 1e097692f55e..bd92a84fc815 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
@@ -56,6 +56,7 @@ import com.android.internal.os.Clock;
import com.android.internal.os.PowerProfile;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MockBatteryStatsImpl;
+import com.android.server.power.stats.NetworkStatsTestUtils;
import com.android.server.power.stats.PowerStatsCollector;
import com.android.server.power.stats.PowerStatsUidResolver;
import com.android.server.power.stats.WifiPowerStatsCollector;
@@ -178,6 +179,11 @@ public class WifiPowerStatsProcessorTest {
public WifiStatsRetriever getWifiStatsRetriever() {
return mWifiStatsRetriever;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 2fe6918630f6..6411463fe0d9 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -94,6 +94,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.IStorageManager;
@@ -181,14 +182,12 @@ public class UserControllerTest {
Intent.ACTION_USER_STARTING);
private static final Set<Integer> START_FOREGROUND_USER_MESSAGE_CODES = newHashSet(
- 0, // for startUserInternalOnHandler
REPORT_USER_SWITCH_MSG,
USER_SWITCH_TIMEOUT_MSG,
USER_START_MSG,
USER_CURRENT_MSG);
private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
- 0, // for startUserInternalOnHandler
USER_START_MSG,
REPORT_LOCKED_BOOT_COMPLETE_MSG);
@@ -376,7 +375,7 @@ public class UserControllerTest {
// and the cascade effect goes on...). In fact, a better approach would to not assert the
// binder calls, but their side effects (in this case, that the user is stopped right away)
assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes())
- .containsExactly(/* for startUserInternalOnHandler */ 0, USER_START_MSG);
+ .containsExactly(USER_START_MSG);
}
private void startUserAssertions(
@@ -419,17 +418,12 @@ public class UserControllerTest {
@Test
public void testDispatchUserSwitch() throws RemoteException {
// Prepare mock observer and register it
- IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
- when(observer.asBinder()).thenReturn(new Binder());
- doAnswer(invocation -> {
- IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
- callback.sendResult(null);
- return null;
- }).when(observer).onUserSwitching(anyInt(), any());
- mUserController.registerUserSwitchObserver(observer, "mock");
+ IUserSwitchObserver observer = registerUserSwitchObserver(
+ /* replyToOnBeforeUserSwitchingCallback= */ true,
+ /* replyToOnUserSwitchingCallback= */ true);
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
- verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
+ verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any());
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -454,13 +448,13 @@ public class UserControllerTest {
@Test
public void testDispatchUserSwitchBadReceiver() throws RemoteException {
- // Prepare mock observer which doesn't notify the callback and register it
- IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
- when(observer.asBinder()).thenReturn(new Binder());
- mUserController.registerUserSwitchObserver(observer, "mock");
+ // Prepare mock observer which doesn't notify the onUserSwitching callback and register it
+ IUserSwitchObserver observer = registerUserSwitchObserver(
+ /* replyToOnBeforeUserSwitchingCallback= */ true,
+ /* replyToOnUserSwitchingCallback= */ false);
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
- verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
+ verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any());
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -551,7 +545,6 @@ public class UserControllerTest {
expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG);
if (backgroundUserStopping) {
expectedCodes.add(CLEAR_USER_JOURNEY_SESSION_MSG);
- expectedCodes.add(0); // this is for directly posting in stopping.
}
if (expectScheduleBackgroundUserStopping) {
expectedCodes.add(SCHEDULED_STOP_BACKGROUND_USER_MSG);
@@ -567,9 +560,9 @@ public class UserControllerTest {
@Test
public void testDispatchUserSwitchComplete() throws RemoteException {
// Prepare mock observer and register it
- IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
- when(observer.asBinder()).thenReturn(new Binder());
- mUserController.registerUserSwitchObserver(observer, "mock");
+ IUserSwitchObserver observer = registerUserSwitchObserver(
+ /* replyToOnBeforeUserSwitchingCallback= */ true,
+ /* replyToOnUserSwitchingCallback= */ true);
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
@@ -1752,6 +1745,29 @@ public class UserControllerTest {
verify(mInjector, never()).onSystemUserVisibilityChanged(anyBoolean());
}
+ private IUserSwitchObserver registerUserSwitchObserver(
+ boolean replyToOnBeforeUserSwitchingCallback, boolean replyToOnUserSwitchingCallback)
+ throws RemoteException {
+ IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
+ when(observer.asBinder()).thenReturn(new Binder());
+ if (replyToOnBeforeUserSwitchingCallback) {
+ doAnswer(invocation -> {
+ IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+ callback.sendResult(null);
+ return null;
+ }).when(observer).onBeforeUserSwitching(anyInt(), any());
+ }
+ if (replyToOnUserSwitchingCallback) {
+ doAnswer(invocation -> {
+ IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+ callback.sendResult(null);
+ return null;
+ }).when(observer).onUserSwitching(anyInt(), any());
+ }
+ mUserController.registerUserSwitchObserver(observer, "mock");
+ return observer;
+ }
+
// Should be public to allow mocking
private static class TestInjector extends UserController.Injector {
public final TestHandler mHandler;
@@ -1957,6 +1973,7 @@ public class UserControllerTest {
* fix this, but in the meantime, this is your warning.
*/
private final List<Message> mMessages = new ArrayList<>();
+ private final List<Runnable> mPendingCallbacks = new ArrayList<>();
TestHandler(Looper looper) {
super(looper);
@@ -1989,14 +2006,24 @@ public class UserControllerTest {
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
- Message copy = new Message();
- copy.copyFrom(msg);
- mMessages.add(copy);
- if (msg.getCallback() != null) {
- msg.getCallback().run();
+ if (msg.getCallback() == null) {
+ Message copy = new Message();
+ copy.copyFrom(msg);
+ mMessages.add(copy);
+ } else {
+ if (SystemClock.uptimeMillis() >= uptimeMillis) {
+ msg.getCallback().run();
+ } else {
+ mPendingCallbacks.add(msg.getCallback());
+ }
msg.setCallback(null);
}
return super.sendMessageAtTime(msg, uptimeMillis);
}
+
+ private void runPendingCallbacks() {
+ mPendingCallbacks.forEach(Runnable::run);
+ mPendingCallbacks.clear();
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 02441648a589..4f551119b42e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -887,6 +887,21 @@ public class HdmiCecLocalDeviceAudioSystemTest {
systemAudioModeRequest_fromAudioSystem);
}
+ @Test
+ public void addAndStartAction_remove() throws Exception {
+ // utilize callback test to test if addAndStartAction(action, remove)
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceAudioSystem.setArcStatus(true);
+ mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+ new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem, callback),
+ true);
+
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiCecLocalDeviceAudioSystem.getActions(
+ ArcTerminationActionFromAvr.class).size()).isEqualTo(1);
+ }
+
private static class TestCallback extends IHdmiControlCallback.Stub {
private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
index 72fa949301cc..085ef53b914f 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
@@ -196,6 +196,8 @@ class OverlayActorEnforcerTests {
},
ActorState.INVALID_OVERLAYABLE_ACTOR_NAME withCases {
fun TestState.mockActor(actorUri: String) {
+ namedActorsMap = mapOf(VALID_NAMESPACE to
+ mapOf(VALID_ACTOR_NAME to VALID_ACTOR_PKG))
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, actorUri)
}
failure("wrongScheme") {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index b6e393d7be0c..03d904283e83 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -342,8 +342,8 @@ public class AppTransitionTests extends WindowTestsBase {
public void testCancelRemoteAnimationWhenFreeze() {
final DisplayContent dc = createNewDisplay(Display.STATE_ON);
doReturn(false).when(dc).onDescendantOrientationChanged(any());
- final WindowState exitingAppWindow = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
- dc, "exiting app");
+ final WindowState exitingAppWindow = newWindowBuilder("exiting app",
+ TYPE_BASE_APPLICATION).setDisplay(dc).build();
final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord;
// Wait until everything in animation handler get executed to prevent the exiting window
// from being removed during WindowSurfacePlacer Traversal.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index 14276ae21899..7033d79d0ee1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -266,10 +266,10 @@ public class DisplayContentDeferredUpdateTests extends WindowTestsBase {
mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
prepareSecondaryDisplay();
- final WindowState defaultDisplayWindow = createWindow(/* parent= */ null,
- TYPE_BASE_APPLICATION, mDisplayContent, "DefaultDisplayWindow");
- final WindowState secondaryDisplayWindow = createWindow(/* parent= */ null,
- TYPE_BASE_APPLICATION, mSecondaryDisplayContent, "SecondaryDisplayWindow");
+ final WindowState defaultDisplayWindow = newWindowBuilder("DefaultDisplayWindow",
+ TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
+ final WindowState secondaryDisplayWindow = newWindowBuilder("SecondaryDisplayWindow",
+ TYPE_BASE_APPLICATION).setDisplay(mSecondaryDisplayContent).build();
makeWindowVisibleAndNotDrawn(defaultDisplayWindow, secondaryDisplayWindow);
// Mark as display switching only for the default display as we filter out
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index bd15bc42e811..347d1bc1becc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -379,13 +379,11 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase {
assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer);
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
- final WindowState secondActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity,
- "firstActivityWin");
+ final WindowState secondActivityWin = newWindowBuilder("secondActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build();
spyOn(secondActivityWin);
// firstActivityWin should be the target
@@ -424,13 +422,11 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase {
setupImeWindow();
final DisplayArea.Tokens imeContainer = mDisplay.getImeContainer();
final WindowToken imeToken = tokenOfType(TYPE_INPUT_METHOD);
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
- final WindowState secondActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity,
- "secondActivityWin");
+ final WindowState secondActivityWin = newWindowBuilder("secondActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build();
spyOn(secondActivityWin);
// firstActivityWin should be the target
@@ -464,9 +460,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase {
assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer);
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
// firstActivityWin should be the target
doReturn(true).when(firstActivityWin).canBeImeTarget();
@@ -499,9 +494,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase {
assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
// firstActivityWin should be the target
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
doReturn(true).when(firstActivityWin).canBeImeTarget();
WindowState imeTarget = mDisplay.computeImeTarget(true /* updateImeTarget */);
@@ -560,8 +554,8 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase {
}
private void setupImeWindow() {
- final WindowState imeWindow = createWindow(null /* parent */,
- TYPE_INPUT_METHOD, mDisplay, "mImeWindow");
+ final WindowState imeWindow = newWindowBuilder("mImeWindow", TYPE_INPUT_METHOD).setDisplay(
+ mDisplay).build();
imeWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
mDisplay.mInputMethodWindow = imeWindow;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index dc4adcc4315b..299717393028 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -878,8 +878,10 @@ public class TaskFragmentTest extends WindowTestsBase {
.build();
final ActivityRecord activity0 = tf0.getTopMostActivity();
final ActivityRecord activity1 = tf1.getTopMostActivity();
- final WindowState win0 = createWindow(null, TYPE_BASE_APPLICATION, activity0, "win0");
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity1, "win1");
+ final WindowState win0 = newWindowBuilder("win0", TYPE_BASE_APPLICATION).setWindowToken(
+ activity0).build();
+ final WindowState win1 = newWindowBuilder("win1", TYPE_BASE_APPLICATION).setWindowToken(
+ activity1).build();
doReturn(false).when(mDisplayContent).shouldImeAttachedToApp();
mDisplayContent.setImeInputTarget(win0);
@@ -1174,8 +1176,8 @@ public class TaskFragmentTest extends WindowTestsBase {
}
private WindowState createAppWindow(ActivityRecord app, String name) {
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, app, name,
- 0 /* ownerId */, false /* ownerCanAddInternalSystemWindow */, new TestIWindow());
+ final WindowState win = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
+ app).setClientWindow(new TestIWindow()).build();
mWm.mWindowMap.put(win.mClient.asBinder(), win);
return win;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
index f145b40d2292..f9250f9ecf5d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -63,7 +63,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
@Test
public void testAppRemoved() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), createSnapshot());
assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
mCache.onAppRemoved(window.mActivityRecord);
@@ -72,7 +72,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
@Test
public void testAppDied() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), createSnapshot());
assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
mCache.onAppDied(window.mActivityRecord);
@@ -81,7 +81,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
@Test
public void testTaskRemoved() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), createSnapshot());
assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
mCache.onIdRemoved(window.getTask().mTaskId);
@@ -90,7 +90,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
@Test
public void testReduced_notCached() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
@@ -105,7 +105,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
@Test
public void testRestoreFromDisk() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
@@ -117,7 +117,7 @@ public class TaskSnapshotCacheTest extends TaskSnapshotPersisterTestBase {
@Test
public void testClearCache() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), mSnapshot);
assertEquals(mSnapshot, mCache.getSnapshot(window.getTask().mTaskId,
false /* isLowResolution */));
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
index c6b2a6b8d42f..1bca53aff034 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -74,8 +74,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase {
@Test
public void testGetClosingApps_closing() {
- final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
- "closingWindow");
+ final WindowState closingWindow = newWindowBuilder("closingWindow",
+ FIRST_APPLICATION_WINDOW).build();
closingWindow.mActivityRecord.commitVisibility(
false /* visible */, true /* performLayout */);
final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
@@ -88,8 +88,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase {
@Test
public void testGetClosingApps_notClosing() {
- final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
- "closingWindow");
+ final WindowState closingWindow = newWindowBuilder("closingWindow",
+ FIRST_APPLICATION_WINDOW).build();
final WindowState openingWindow = createAppWindow(closingWindow.getTask(),
FIRST_APPLICATION_WINDOW, "openingWindow");
closingWindow.mActivityRecord.commitVisibility(
@@ -105,8 +105,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase {
@Test
public void testGetClosingApps_skipClosingAppsSnapshotTasks() {
- final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
- "closingWindow");
+ final WindowState closingWindow = newWindowBuilder("closingWindow",
+ FIRST_APPLICATION_WINDOW).build();
closingWindow.mActivityRecord.commitVisibility(
false /* visible */, true /* performLayout */);
final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
@@ -133,19 +133,19 @@ public class TaskSnapshotControllerTest extends WindowTestsBase {
@Test
public void testGetSnapshotMode() {
- final WindowState disabledWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow");
+ final WindowState disabledWindow = newWindowBuilder("disabledWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
disabledWindow.mActivityRecord.setRecentsScreenshotEnabled(false);
assertEquals(SNAPSHOT_MODE_APP_THEME,
mWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask()));
- final WindowState normalWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ final WindowState normalWindow = newWindowBuilder("normalWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
assertEquals(SNAPSHOT_MODE_REAL,
mWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask()));
- final WindowState secureWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow");
+ final WindowState secureWindow = newWindowBuilder("secureWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
secureWindow.mAttrs.flags |= FLAG_SECURE;
assertEquals(SNAPSHOT_MODE_APP_THEME,
mWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
@@ -297,8 +297,8 @@ public class TaskSnapshotControllerTest extends WindowTestsBase {
spyOn(mWm.mTaskSnapshotController);
doReturn(false).when(mWm.mTaskSnapshotController).shouldDisableSnapshots();
- final WindowState normalWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ final WindowState normalWindow = newWindowBuilder("normalWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
final TaskSnapshot snapshot = new TaskSnapshotPersisterTestBase.TaskSnapshotBuilder()
.setTopActivityComponent(normalWindow.mActivityRecord.mActivityComponent).build();
doReturn(snapshot).when(mWm.mTaskSnapshotController).snapshot(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
index 9bde0663d4a3..51ea498811fc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
@@ -41,7 +41,7 @@ import java.io.File;
* Test class for {@link TaskSnapshotPersister} and {@link AppSnapshotLoader}
*
* Build/Install/Run:
- * atest TaskSnapshotPersisterLoaderTest
+ * atest TaskSnapshotLowResDisabledTest
*/
@MediumTest
@Presubmit
@@ -126,7 +126,7 @@ public class TaskSnapshotLowResDisabledTest extends TaskSnapshotPersisterTestBas
@Test
public void testReduced_notCached() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 1fa657822189..5ed2df30518b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -504,8 +504,8 @@ public class WindowContainerTests extends WindowTestsBase {
assertTrue(child.isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION));
assertFalse(child.isAnimating(PARENTS, ANIMATION_TYPE_SCREEN_ROTATION));
- final WindowState windowState = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
- mDisplayContent, "TestWindowState");
+ final WindowState windowState = newWindowBuilder("TestWindowState",
+ TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
WindowContainer parent = windowState.getParent();
spyOn(windowState.mSurfaceAnimator);
doReturn(true).when(windowState.mSurfaceAnimator).isAnimating();
@@ -1045,8 +1045,8 @@ public class WindowContainerTests extends WindowTestsBase {
// An animating window with mRemoveOnExit can be removed by handleCompleteDeferredRemoval
// once it no longer animates.
- final WindowState exitingWindow = createWindow(null, TYPE_APPLICATION_OVERLAY,
- displayContent, "exiting window");
+ final WindowState exitingWindow = newWindowBuilder("exiting window",
+ TYPE_APPLICATION_OVERLAY).setDisplay(displayContent).build();
exitingWindow.startAnimation(exitingWindow.getPendingTransaction(),
mock(AnimationAdapter.class), false /* hidden */,
SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION);
@@ -1063,7 +1063,7 @@ public class WindowContainerTests extends WindowTestsBase {
final ActivityRecord r = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setDisplay(displayContent).build().getTopMostActivity();
// Add a window and make the activity animating so the removal of activity is deferred.
- createWindow(null, TYPE_BASE_APPLICATION, r, "win");
+ newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(r).build();
doReturn(true).when(r).isAnimating(anyInt(), anyInt());
displayContent.remove();
@@ -1216,7 +1216,8 @@ public class WindowContainerTests extends WindowTestsBase {
public void testFreezeInsets() {
final Task task = createTask(mDisplayContent);
final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(
+ activity).build();
// Set visibility to false, verify the main window of the task will be set the frozen
// insets state immediately.
@@ -1233,7 +1234,8 @@ public class WindowContainerTests extends WindowTestsBase {
final Task rootTask = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(
+ activity).build();
task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE);
spyOn(win);
doReturn(true).when(task).okToAnimate();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
index 72935cb546d9..8606581539ff 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -49,9 +49,10 @@ public class WindowContainerTraversalTests extends WindowTestsBase {
@SetupWindows(addWindows = { W_DOCK_DIVIDER, W_INPUT_METHOD })
@Test
public void testDockedDividerPosition() {
- final WindowState splitScreenWindow = createWindow(null,
- WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
- mDisplayContent, "splitScreenWindow");
+ final WindowState splitScreenWindow = newWindowBuilder("splitScreenWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+ mDisplayContent).build();
mDisplayContent.setImeLayeringTarget(splitScreenWindow);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 50e0e181cd68..513ba1d49258 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -154,9 +154,11 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testIsParentWindowHidden() {
- final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
- final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+ final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
// parentWindow is initially set to hidden.
assertTrue(parentWindow.mHidden);
@@ -172,10 +174,12 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testIsChildWindow() {
- final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
- final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
- final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+ final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build();
assertFalse(parentWindow.isChildWindow());
assertTrue(child1.isChildWindow());
@@ -185,12 +189,15 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testHasChild() {
- final WindowState win1 = createWindow(null, TYPE_APPLICATION, "win1");
- final WindowState win11 = createWindow(win1, FIRST_SUB_WINDOW, "win11");
- final WindowState win12 = createWindow(win1, FIRST_SUB_WINDOW, "win12");
- final WindowState win2 = createWindow(null, TYPE_APPLICATION, "win2");
- final WindowState win21 = createWindow(win2, FIRST_SUB_WINDOW, "win21");
- final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+ final WindowState win1 = newWindowBuilder("win1", TYPE_APPLICATION).build();
+ final WindowState win11 = newWindowBuilder("win11", FIRST_SUB_WINDOW).setParent(
+ win1).build();
+ final WindowState win12 = newWindowBuilder("win12", FIRST_SUB_WINDOW).setParent(
+ win1).build();
+ final WindowState win2 = newWindowBuilder("win2", TYPE_APPLICATION).build();
+ final WindowState win21 = newWindowBuilder("win21", FIRST_SUB_WINDOW).setParent(
+ win2).build();
+ final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build();
assertTrue(win1.hasChild(win11));
assertTrue(win1.hasChild(win12));
@@ -206,9 +213,11 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testGetParentWindow() {
- final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
- final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+ final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
assertNull(parentWindow.getParentWindow());
assertEquals(parentWindow, child1.getParentWindow());
@@ -217,8 +226,8 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testOverlayWindowHiddenWhenSuspended() {
- final WindowState overlayWindow = spy(createWindow(null, TYPE_APPLICATION_OVERLAY,
- "overlayWindow"));
+ final WindowState overlayWindow = spy(
+ newWindowBuilder("overlayWindow", TYPE_APPLICATION_OVERLAY).build());
overlayWindow.setHiddenWhileSuspended(true);
verify(overlayWindow).hide(true /* doAnimation */, true /* requestAnim */);
overlayWindow.setHiddenWhileSuspended(false);
@@ -227,9 +236,11 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testGetTopParentWindow() {
- final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
- final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+ final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ root).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ child1).build();
assertEquals(root, root.getTopParentWindow());
assertEquals(root, child1.getTopParentWindow());
@@ -244,7 +255,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testIsOnScreen_hiddenByPolicy() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
window.setHasSurface(true);
assertTrue(window.isOnScreen());
window.hide(false /* doAnimation */, false /* requestAnim */);
@@ -273,8 +284,8 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testCanBeImeTarget() {
- final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
- final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow");
+ final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build();
+ final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_INPUT_METHOD).build();
// Setting FLAG_NOT_FOCUSABLE prevents the window from being an IME target.
appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
@@ -328,16 +339,17 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testGetWindow() {
- final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
- final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild");
- final WindowState mediaOverlayChild = createWindow(root,
- TYPE_APPLICATION_MEDIA_OVERLAY, "mediaOverlayChild");
- final WindowState attachedDialogChild = createWindow(root,
- TYPE_APPLICATION_ATTACHED_DIALOG, "attachedDialogChild");
- final WindowState subPanelChild = createWindow(root,
- TYPE_APPLICATION_SUB_PANEL, "subPanelChild");
- final WindowState aboveSubPanelChild = createWindow(root,
- TYPE_APPLICATION_ABOVE_SUB_PANEL, "aboveSubPanelChild");
+ final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+ final WindowState mediaChild = newWindowBuilder("mediaChild",
+ TYPE_APPLICATION_MEDIA).setParent(root).build();
+ final WindowState mediaOverlayChild = newWindowBuilder("mediaOverlayChild",
+ TYPE_APPLICATION_MEDIA_OVERLAY).setParent(root).build();
+ final WindowState attachedDialogChild = newWindowBuilder("attachedDialogChild",
+ TYPE_APPLICATION_ATTACHED_DIALOG).setParent(root).build();
+ final WindowState subPanelChild = newWindowBuilder("subPanelChild",
+ TYPE_APPLICATION_SUB_PANEL).setParent(root).build();
+ final WindowState aboveSubPanelChild = newWindowBuilder("aboveSubPanelChild",
+ TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(root).build();
final LinkedList<WindowState> windows = new LinkedList<>();
@@ -358,7 +370,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testDestroySurface() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
win.mHasSurface = win.mAnimatingExit = true;
win.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class);
win.onExitAnimationDone();
@@ -384,8 +396,10 @@ public class WindowStateTests extends WindowTestsBase {
// Call prepareWindowToDisplayDuringRelayout for a window without FLAG_TURN_SCREEN_ON before
// calling setCurrentLaunchCanTurnScreenOn for windows with flag in the same activity.
final ActivityRecord activity = createActivityRecord(mDisplayContent);
- final WindowState first = createWindow(null, TYPE_APPLICATION, activity, "first");
- final WindowState second = createWindow(null, TYPE_APPLICATION, activity, "second");
+ final WindowState first = newWindowBuilder("first", TYPE_APPLICATION).setWindowToken(
+ activity).build();
+ final WindowState second = newWindowBuilder("second", TYPE_APPLICATION).setWindowToken(
+ activity).build();
testPrepareWindowToDisplayDuringRelayout(first, false /* expectedWakeupCalled */,
true /* expectedCurrentLaunchCanTurnScreenOn */);
@@ -423,10 +437,10 @@ public class WindowStateTests extends WindowTestsBase {
// Call prepareWindowToDisplayDuringRelayout for a windows that are not children of an
// activity. Both windows have the FLAG_TURNS_SCREEN_ON so both should call wakeup
final WindowToken windowToken = createTestWindowToken(FIRST_SUB_WINDOW, mDisplayContent);
- final WindowState firstWindow = createWindow(null, TYPE_APPLICATION, windowToken,
- "firstWindow");
- final WindowState secondWindow = createWindow(null, TYPE_APPLICATION, windowToken,
- "secondWindow");
+ final WindowState firstWindow = newWindowBuilder("firstWindow",
+ TYPE_APPLICATION).setWindowToken(windowToken).build();
+ final WindowState secondWindow = newWindowBuilder("secondWindow",
+ TYPE_APPLICATION).setWindowToken(windowToken).build();
firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
@@ -459,7 +473,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testCanAffectSystemUiFlags() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mActivityRecord.setVisible(true);
assertTrue(app.canAffectSystemUiFlags());
app.mActivityRecord.setVisible(false);
@@ -471,7 +485,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testCanAffectSystemUiFlags_starting() {
- final WindowState app = createWindow(null, TYPE_APPLICATION_STARTING, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION_STARTING).build();
app.mActivityRecord.setVisible(true);
app.mStartingData = new SnapshotStartingData(mWm, null, 0);
assertFalse(app.canAffectSystemUiFlags());
@@ -481,7 +495,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testCanAffectSystemUiFlags_disallow() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mActivityRecord.setVisible(true);
assertTrue(app.canAffectSystemUiFlags());
app.getTask().setCanAffectSystemUiFlags(false);
@@ -538,9 +552,11 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testIsSelfOrAncestorWindowAnimating() {
- final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
- final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+ final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ root).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ child1).build();
assertFalse(child2.isSelfOrAncestorWindowAnimatingExit());
child2.mAnimatingExit = true;
assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
@@ -551,7 +567,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testDeferredRemovalByAnimating() {
- final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
+ final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build();
makeWindowVisible(appWindow);
spyOn(appWindow.mWinAnimator);
doReturn(true).when(appWindow.mWinAnimator).getShown();
@@ -571,8 +587,9 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testOnExitAnimationDone() {
- final WindowState parent = createWindow(null, TYPE_APPLICATION, "parent");
- final WindowState child = createWindow(parent, TYPE_APPLICATION_PANEL, "child");
+ final WindowState parent = newWindowBuilder("parent", TYPE_APPLICATION).build();
+ final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent(
+ parent).build();
final SurfaceControl.Transaction t = parent.getPendingTransaction();
child.startAnimation(t, mock(AnimationAdapter.class), false /* hidden */,
SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION);
@@ -609,7 +626,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testLayoutSeqResetOnReparent() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mLayoutSeq = 1;
mDisplayContent.mLayoutSeq = 1;
@@ -622,7 +639,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testDisplayIdUpdatedOnReparent() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
// fake a different display
app.mInputWindowHandle.setDisplayId(mDisplayContent.getDisplayId() + 1);
app.onDisplayChanged(mDisplayContent);
@@ -633,7 +650,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testApplyWithNextDraw() {
- final WindowState win = createWindow(null, TYPE_APPLICATION_OVERLAY, "app");
+ final WindowState win = newWindowBuilder("app", TYPE_APPLICATION_OVERLAY).build();
final SurfaceControl.Transaction[] handledT = { null };
// The normal case that the draw transaction is applied with finishing drawing.
win.applyWithNextDraw(t -> handledT[0] = t);
@@ -657,7 +674,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testSeamlesslyRotateWindow() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
final SurfaceControl.Transaction t = spy(StubTransaction.class);
makeWindowVisible(app);
@@ -707,7 +724,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testVisibilityChangeSwitchUser() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState window = newWindowBuilder("app", TYPE_APPLICATION).build();
window.mHasSurface = true;
spyOn(window);
doReturn(false).when(window).showForAllUsers();
@@ -729,8 +746,9 @@ public class WindowStateTests extends WindowTestsBase {
final CompatModePackages cmp = mWm.mAtmService.mCompatModePackages;
spyOn(cmp);
doReturn(overrideScale).when(cmp).getCompatScale(anyString(), anyInt());
- final WindowState w = createWindow(null, TYPE_APPLICATION_OVERLAY, "win");
- final WindowState child = createWindow(w, TYPE_APPLICATION_PANEL, "child");
+ final WindowState w = newWindowBuilder("win", TYPE_APPLICATION_OVERLAY).build();
+ final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent(
+ w).build();
assertTrue(w.hasCompatScale());
assertTrue(child.hasCompatScale());
@@ -788,7 +806,8 @@ public class WindowStateTests extends WindowTestsBase {
// Child window without scale (e.g. different app) should apply inverse scale of parent.
doReturn(1f).when(cmp).getCompatScale(anyString(), anyInt());
- final WindowState child2 = createWindow(w, TYPE_APPLICATION_SUB_PANEL, "child2");
+ final WindowState child2 = newWindowBuilder("child2", TYPE_APPLICATION_SUB_PANEL).setParent(
+ w).build();
makeWindowVisible(w, child2);
clearInvocations(t);
child2.prepareSurfaces();
@@ -798,10 +817,10 @@ public class WindowStateTests extends WindowTestsBase {
@SetupWindows(addWindows = { W_ABOVE_ACTIVITY, W_NOTIFICATION_SHADE })
@Test
public void testRequestDrawIfNeeded() {
- final WindowState startingApp = createWindow(null /* parent */,
- TYPE_BASE_APPLICATION, "startingApp");
- final WindowState startingWindow = createWindow(null /* parent */,
- TYPE_APPLICATION_STARTING, startingApp.mToken, "starting");
+ final WindowState startingApp = newWindowBuilder("startingApp",
+ TYPE_BASE_APPLICATION).build();
+ final WindowState startingWindow = newWindowBuilder("starting",
+ TYPE_APPLICATION_STARTING).setWindowToken(startingApp.mToken).build();
startingApp.mActivityRecord.mStartingWindow = startingWindow;
final WindowState keyguardHostWindow = mNotificationShadeWindow;
final WindowState allDrawnApp = mAppWindow;
@@ -878,7 +897,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testRequestResizeForBlastSync() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState win = newWindowBuilder("window", TYPE_APPLICATION).build();
makeWindowVisible(win);
makeLastConfigReportedToClient(win, true /* visible */);
win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
@@ -926,8 +945,8 @@ public class WindowStateTests extends WindowTestsBase {
final Task task = createTask(mDisplayContent);
final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
- final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity,
- "App window");
+ final WindowState win = newWindowBuilder("App window", TYPE_APPLICATION).setWindowToken(
+ embeddedActivity).build();
doReturn(true).when(embeddedActivity).isVisible();
embeddedActivity.setVisibleRequested(true);
makeWindowVisible(win);
@@ -949,14 +968,14 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testCantReceiveTouchWhenAppTokenHiddenRequested() {
- final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
+ final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build();
win0.mActivityRecord.setVisibleRequested(false);
assertFalse(win0.canReceiveTouchInput());
}
@Test
public void testCantReceiveTouchWhenNotFocusable() {
- final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
+ final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build();
final Task rootTask = win0.mActivityRecord.getRootTask();
spyOn(rootTask);
when(rootTask.shouldIgnoreInput()).thenReturn(true);
@@ -969,7 +988,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testUpdateInputWindowHandle() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
win.mAttrs.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
win.mAttrs.flags = FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH;
final InputWindowHandle handle = new InputWindowHandle(
@@ -1026,7 +1045,7 @@ public class WindowStateTests extends WindowTestsBase {
@DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
@Test
public void testTouchRegionUsesLetterboxBoundsIfTransformedBoundsAndLetterboxScrolling() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
// Transformed bounds used for size of touchable region if letterbox inner bounds are empty.
final Rect transformedBounds = new Rect(0, 0, 300, 500);
@@ -1051,7 +1070,7 @@ public class WindowStateTests extends WindowTestsBase {
@DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
@Test
public void testTouchRegionUsesLetterboxBoundsIfNullTransformedBoundsAndLetterboxScrolling() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
// Fragment bounds used for size of touchable region if letterbox inner bounds are empty
// and Transform bounds are null.
@@ -1083,7 +1102,7 @@ public class WindowStateTests extends WindowTestsBase {
@EnableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
@Test
public void testTouchRegionUsesTransformedBoundsIfLetterboxScrolling() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
// Transformed bounds used for size of touchable region if letterbox inner bounds are empty.
final Rect transformedBounds = new Rect(0, 0, 300, 500);
@@ -1109,7 +1128,7 @@ public class WindowStateTests extends WindowTestsBase {
public void testHasActiveVisibleWindow() {
final int uid = ActivityBuilder.DEFAULT_FAKE_UID;
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app", uid);
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).setOwnerId(uid).build();
app.mActivityRecord.setVisible(false);
app.mActivityRecord.setVisibility(false);
assertFalse(mAtm.hasActiveVisibleWindow(uid));
@@ -1120,15 +1139,17 @@ public class WindowStateTests extends WindowTestsBase {
// Make the activity invisible and add a visible toast. The uid should have no active
// visible window because toast can be misused by legacy app to bypass background check.
app.mActivityRecord.setVisibility(false);
- final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay", uid);
- final WindowState toast = createWindow(null, TYPE_TOAST, app.mToken, "toast", uid);
+ final WindowState overlay = newWindowBuilder("overlay",
+ TYPE_APPLICATION_OVERLAY).setOwnerId(uid).build();
+ final WindowState toast = newWindowBuilder("toast", TYPE_TOAST).setWindowToken(
+ app.mToken).setOwnerId(uid).build();
toast.onSurfaceShownChanged(true);
assertFalse(mAtm.hasActiveVisibleWindow(uid));
// Though starting window should belong to system. Make sure it is ignored to avoid being
// allow-list unexpectedly, see b/129563343.
- final WindowState starting =
- createWindow(null, TYPE_APPLICATION_STARTING, app.mToken, "starting", uid);
+ final WindowState starting = newWindowBuilder("starting",
+ TYPE_APPLICATION_STARTING).setWindowToken(app.mToken).setOwnerId(uid).build();
starting.onSurfaceShownChanged(true);
assertFalse(mAtm.hasActiveVisibleWindow(uid));
@@ -1145,8 +1166,8 @@ public class WindowStateTests extends WindowTestsBase {
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
@Test
public void testNeedsRelativeLayeringToIme_notAttached() {
- WindowState sameTokenWindow = createWindow(null, TYPE_BASE_APPLICATION, mAppWindow.mToken,
- "SameTokenWindow");
+ WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow",
+ TYPE_BASE_APPLICATION).setWindowToken(mAppWindow.mToken).build();
mDisplayContent.setImeLayeringTarget(mAppWindow);
makeWindowVisible(mImeWindow);
sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1158,8 +1179,8 @@ public class WindowStateTests extends WindowTestsBase {
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
@Test
public void testNeedsRelativeLayeringToIme_startingWindow() {
- WindowState sameTokenWindow = createWindow(null, TYPE_APPLICATION_STARTING,
- mAppWindow.mToken, "SameTokenWindow");
+ WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow",
+ TYPE_APPLICATION_STARTING).setWindowToken(mAppWindow.mToken).build();
mDisplayContent.setImeLayeringTarget(mAppWindow);
makeWindowVisible(mImeWindow);
sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1169,9 +1190,9 @@ public class WindowStateTests extends WindowTestsBase {
@UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD})
@Test
public void testNeedsRelativeLayeringToIme_systemDialog() {
- WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
- mDisplayContent,
- "SystemDialog", true);
+ WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+ TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
mDisplayContent.setImeLayeringTarget(mAppWindow);
mAppWindow.getTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
makeWindowVisible(mImeWindow);
@@ -1182,20 +1203,21 @@ public class WindowStateTests extends WindowTestsBase {
@UseTestDisplay(addWindows = {W_INPUT_METHOD})
@Test
public void testNeedsRelativeLayeringToIme_notificationShadeShouldNotHideSystemDialog() {
- WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
- mDisplayContent,
- "SystemDialog", true);
+ WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+ TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
mDisplayContent.setImeLayeringTarget(systemDialogWindow);
makeWindowVisible(mImeWindow);
- WindowState notificationShade = createWindow(null, TYPE_NOTIFICATION_SHADE,
- mDisplayContent, "NotificationShade", true);
+ WindowState notificationShade = newWindowBuilder("NotificationShade",
+ TYPE_NOTIFICATION_SHADE).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
notificationShade.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
assertFalse(notificationShade.needsRelativeLayeringToIme());
}
@Test
public void testSetFreezeInsetsState() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
spyOn(app);
doReturn(true).when(app).isVisible();
@@ -1216,7 +1238,7 @@ public class WindowStateTests extends WindowTestsBase {
verify(app).notifyInsetsChanged();
// Verify that invisible non-activity window won't dispatch insets changed.
- final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay");
+ final WindowState overlay = newWindowBuilder("overlay", TYPE_APPLICATION_OVERLAY).build();
makeWindowVisible(overlay);
assertTrue(overlay.isReadyToDispatchInsetsState());
overlay.mHasSurface = false;
@@ -1244,9 +1266,9 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testAdjustImeInsetsVisibilityWhenSwitchingApps() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2");
- final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
+ final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build();
+ final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build();
spyOn(imeWindow);
doReturn(true).when(imeWindow).isVisible();
mDisplayContent.mInputMethodWindow = imeWindow;
@@ -1279,10 +1301,11 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testAdjustImeInsetsVisibilityWhenSwitchingApps_toAppInMultiWindowMode() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- final WindowState app2 = createWindow(null, WINDOWING_MODE_MULTI_WINDOW,
- ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app2");
- final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
+ final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+ mDisplayContent).build();
+ final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build();
spyOn(imeWindow);
doReturn(true).when(imeWindow).isVisible();
mDisplayContent.mInputMethodWindow = imeWindow;
@@ -1321,8 +1344,8 @@ public class WindowStateTests extends WindowTestsBase {
@SetupWindows(addWindows = W_ACTIVITY)
@Test
public void testUpdateImeControlTargetWhenLeavingMultiWindow() {
- WindowState app = createWindow(null, TYPE_BASE_APPLICATION,
- mAppWindow.mToken, "app");
+ WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken(
+ mAppWindow.mToken).build();
mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController());
spyOn(app);
@@ -1349,8 +1372,8 @@ public class WindowStateTests extends WindowTestsBase {
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD, W_NOTIFICATION_SHADE })
@Test
public void testNotificationShadeHasImeInsetsWhenMultiWindow() {
- WindowState app = createWindow(null, TYPE_BASE_APPLICATION,
- mAppWindow.mToken, "app");
+ WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken(
+ mAppWindow.mToken).build();
// Simulate entering multi-window mode and windowing mode is multi-window.
app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1376,7 +1399,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testRequestedVisibility() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mActivityRecord.setVisible(false);
app.mActivityRecord.setVisibility(false);
assertFalse(app.isVisibleRequested());
@@ -1391,7 +1414,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testKeepClearAreas() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
makeWindowVisible(window);
final Rect keepClearArea1 = new Rect(0, 0, 10, 10);
@@ -1433,7 +1456,7 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testUnrestrictedKeepClearAreas() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
makeWindowVisible(window);
final Rect keepClearArea1 = new Rect(0, 0, 10, 10);
@@ -1481,8 +1504,9 @@ public class WindowStateTests extends WindowTestsBase {
final InputMethodManagerInternal immi = InputMethodManagerInternal.get();
spyOn(immi);
- final WindowState imeTarget = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
- createActivityRecord(mDisplayContent), "imeTarget");
+ final WindowState imeTarget = newWindowBuilder("imeTarget",
+ TYPE_BASE_APPLICATION).setWindowToken(
+ createActivityRecord(mDisplayContent)).build();
imeTarget.mActivityRecord.setVisibleRequested(true);
makeWindowVisible(imeTarget);
@@ -1562,8 +1586,8 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testIsSecureLocked_flagSecureSet() {
- WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
- 1 /* ownerId */);
+ WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+ 1).build();
window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE;
assertTrue(window.isSecureLocked());
@@ -1571,8 +1595,8 @@ public class WindowStateTests extends WindowTestsBase {
@Test
public void testIsSecureLocked_flagSecureNotSet() {
- WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
- 1 /* ownerId */);
+ WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+ 1).build();
assertFalse(window.isSecureLocked());
}
@@ -1581,8 +1605,8 @@ public class WindowStateTests extends WindowTestsBase {
public void testIsSecureLocked_disableSecureWindows() {
assumeTrue(Build.IS_DEBUGGABLE);
- WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
- 1 /* ownerId */);
+ WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+ 1).build();
window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE;
ContentResolver cr = useFakeSettingsProvider();
@@ -1617,8 +1641,10 @@ public class WindowStateTests extends WindowTestsBase {
String testPackage = "test";
int ownerId1 = 20;
int ownerId2 = 21;
- final WindowState window1 = createWindow(null, TYPE_APPLICATION, "window1", ownerId1);
- final WindowState window2 = createWindow(null, TYPE_APPLICATION, "window2", ownerId2);
+ final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setOwnerId(
+ ownerId1).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setOwnerId(
+ ownerId2).build();
// Setting packagename for targeted feature
window1.mAttrs.packageName = testPackage;
@@ -1638,7 +1664,8 @@ public class WindowStateTests extends WindowTestsBase {
public void testIsSecureLocked_sensitiveContentBlockOrClearScreenCaptureForApp() {
String testPackage = "test";
int ownerId = 20;
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window", ownerId);
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).setOwnerId(
+ ownerId).build();
window.mAttrs.packageName = testPackage;
assertFalse(window.isSecureLocked());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
index f226b9d29ca0..a02c3db1e636 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
@@ -74,11 +74,16 @@ public class WindowTokenTests extends WindowTestsBase {
assertEquals(0, token.getWindowsCount());
- final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
- final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, token, "window11");
- final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, token, "window12");
- final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
- final WindowState window3 = createWindow(null, TYPE_APPLICATION, token, "window3");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken(
+ token).build();
+ final WindowState window11 = newWindowBuilder("window11", FIRST_SUB_WINDOW).setParent(
+ window1).setWindowToken(token).build();
+ final WindowState window12 = newWindowBuilder("window12", FIRST_SUB_WINDOW).setParent(
+ window1).setWindowToken(token).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken(
+ token).build();
+ final WindowState window3 = newWindowBuilder("window3", TYPE_APPLICATION).setWindowToken(
+ token).build();
token.addWindow(window1);
// NOTE: Child windows will not be added to the token as window containers can only
@@ -105,8 +110,10 @@ public class WindowTokenTests extends WindowTestsBase {
public void testAddWindow_assignsLayers() {
final TestWindowToken token1 = createTestWindowToken(0, mDisplayContent);
final TestWindowToken token2 = createTestWindowToken(0, mDisplayContent);
- final WindowState window1 = createWindow(null, TYPE_STATUS_BAR, token1, "window1");
- final WindowState window2 = createWindow(null, TYPE_STATUS_BAR, token2, "window2");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_STATUS_BAR).setWindowToken(
+ token1).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_STATUS_BAR).setWindowToken(
+ token2).build();
token1.addWindow(window1);
token2.addWindow(window2);
@@ -122,8 +129,10 @@ public class WindowTokenTests extends WindowTestsBase {
assertEquals(token, dc.getWindowToken(token.token));
- final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
- final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken(
+ token).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken(
+ token).build();
window2.removeImmediately();
// The token should still be mapped in the display content since it still has a child.
@@ -147,8 +156,10 @@ public class WindowTokenTests extends WindowTestsBase {
// Verify that the token is on the display
assertNotNull(mDisplayContent.getWindowToken(token.token));
- final WindowState window1 = createWindow(null, TYPE_TOAST, token, "window1");
- final WindowState window2 = createWindow(null, TYPE_TOAST, token, "window2");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_TOAST).setWindowToken(
+ token).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_TOAST).setWindowToken(
+ token).build();
mDisplayContent.removeWindowToken(token.token, true /* animateExit */);
// Verify that the token is no longer mapped on the display
@@ -231,7 +242,8 @@ public class WindowTokenTests extends WindowTestsBase {
assertNull(fromClientToken.mSurfaceControl);
- createWindow(null, TYPE_APPLICATION_OVERLAY, fromClientToken, "window");
+ newWindowBuilder("window", TYPE_APPLICATION_OVERLAY).setWindowToken(
+ fromClientToken).build();
assertNotNull(fromClientToken.mSurfaceControl);
final WindowToken nonClientToken = new WindowToken.Builder(mDisplayContent.mWmService,
@@ -285,7 +297,7 @@ public class WindowTokenTests extends WindowTestsBase {
// Simulate an app window to be the IME layering target, assume the app window has no
// frozen insets state by default.
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
mDisplayContent.setImeLayeringTarget(app);
assertNull(app.getFrozenInsetsState());
assertTrue(app.isImeLayeringTarget());
@@ -299,7 +311,8 @@ public class WindowTokenTests extends WindowTestsBase {
@Test
public void testRemoveWindowToken_noAnimateExitWhenSet() {
final TestWindowToken token = createTestWindowToken(0, mDisplayContent);
- final WindowState win = createWindow(null, TYPE_APPLICATION, token, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).setWindowToken(
+ token).build();
makeWindowVisible(win);
assertTrue(win.isOnScreen());
spyOn(win);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 4f60106db93d..84e21181a7b9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -221,7 +221,7 @@ public class ZOrderingTests extends WindowTestsBase {
}
WindowState createWindow(String name) {
- return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name);
+ return newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
}
@Test
@@ -263,12 +263,12 @@ public class ZOrderingTests extends WindowTestsBase {
@Test
public void testAssignWindowLayers_ForImeWithAppTargetWithChildWindows() {
final WindowState imeAppTarget = createWindow("imeAppTarget");
- final WindowState imeAppTargetChildAboveWindow = createWindow(imeAppTarget,
- TYPE_APPLICATION_ATTACHED_DIALOG, imeAppTarget.mToken,
- "imeAppTargetChildAboveWindow");
- final WindowState imeAppTargetChildBelowWindow = createWindow(imeAppTarget,
- TYPE_APPLICATION_MEDIA_OVERLAY, imeAppTarget.mToken,
- "imeAppTargetChildBelowWindow");
+ final WindowState imeAppTargetChildAboveWindow = newWindowBuilder(
+ "imeAppTargetChildAboveWindow", TYPE_APPLICATION_ATTACHED_DIALOG).setParent(
+ imeAppTarget).setWindowToken(imeAppTarget.mToken).build();
+ final WindowState imeAppTargetChildBelowWindow = newWindowBuilder(
+ "imeAppTargetChildBelowWindow", TYPE_APPLICATION_MEDIA_OVERLAY).setParent(
+ imeAppTarget).setWindowToken(imeAppTarget.mToken).build();
mDisplayContent.setImeLayeringTarget(imeAppTarget);
makeWindowVisible(mImeWindow);
@@ -313,9 +313,9 @@ public class ZOrderingTests extends WindowTestsBase {
@Test
public void testAssignWindowLayers_ForImeNonAppImeTarget() {
- final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY,
- mDisplayContent, "imeSystemOverlayTarget",
- true /* ownerCanAddInternalSystemWindow */);
+ final WindowState imeSystemOverlayTarget = newWindowBuilder("imeSystemOverlayTarget",
+ TYPE_SYSTEM_OVERLAY).setDisplay(mDisplayContent).setOwnerCanAddInternalSystemWindow(
+ true).build();
mDisplayContent.setImeLayeringTarget(imeSystemOverlayTarget);
mDisplayContent.assignChildLayers(mTransaction);
@@ -354,18 +354,19 @@ public class ZOrderingTests extends WindowTestsBase {
@Test
public void testStackLayers() {
final WindowState anyWindow1 = createWindow("anyWindow");
- final WindowState pinnedStackWindow = createWindow(null, WINDOWING_MODE_PINNED,
- ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
- "pinnedStackWindow");
- final WindowState dockedStackWindow = createWindow(null,
- WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
- mDisplayContent, "dockedStackWindow");
- final WindowState assistantStackWindow = createWindow(null,
- WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
- mDisplayContent, "assistantStackWindow");
- final WindowState homeActivityWindow = createWindow(null, WINDOWING_MODE_FULLSCREEN,
- ACTIVITY_TYPE_HOME, TYPE_BASE_APPLICATION,
- mDisplayContent, "homeActivityWindow");
+ final WindowState pinnedStackWindow = newWindowBuilder("pinnedStackWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_PINNED).setActivityType(
+ ACTIVITY_TYPE_STANDARD).setDisplay(mDisplayContent).build();
+ final WindowState dockedStackWindow = newWindowBuilder("dockedStackWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+ mDisplayContent).build();
+ final WindowState assistantStackWindow = newWindowBuilder("assistantStackWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType(
+ ACTIVITY_TYPE_ASSISTANT).setDisplay(mDisplayContent).build();
+ final WindowState homeActivityWindow = newWindowBuilder("homeActivityWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType(
+ ACTIVITY_TYPE_HOME).setDisplay(mDisplayContent).build();
final WindowState anyWindow2 = createWindow("anyWindow2");
mDisplayContent.assignChildLayers(mTransaction);
@@ -383,13 +384,12 @@ public class ZOrderingTests extends WindowTestsBase {
@Test
public void testAssignWindowLayers_ForSysUiPanels() {
- final WindowState navBarPanel =
- createWindow(null, TYPE_NAVIGATION_BAR_PANEL, mDisplayContent, "NavBarPanel");
- final WindowState statusBarPanel =
- createWindow(null, TYPE_STATUS_BAR_ADDITIONAL, mDisplayContent,
- "StatusBarAdditional");
- final WindowState statusBarSubPanel =
- createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, mDisplayContent, "StatusBarSubPanel");
+ final WindowState navBarPanel = newWindowBuilder("NavBarPanel",
+ TYPE_NAVIGATION_BAR_PANEL).setDisplay(mDisplayContent).build();
+ final WindowState statusBarPanel = newWindowBuilder("StatusBarAdditional",
+ TYPE_STATUS_BAR_ADDITIONAL).setDisplay(mDisplayContent).build();
+ final WindowState statusBarSubPanel = newWindowBuilder("StatusBarSubPanel",
+ TYPE_STATUS_BAR_SUB_PANEL).setDisplay(mDisplayContent).build();
mDisplayContent.assignChildLayers(mTransaction);
// Ime should be above all app windows and below system windows if it is targeting an app
@@ -401,15 +401,16 @@ public class ZOrderingTests extends WindowTestsBase {
@Test
public void testAssignWindowLayers_ForImeOnPopupImeLayeringTarget() {
- final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION,
- mAppWindow.mActivityRecord, "imeAppTarget");
+ final WindowState imeAppTarget = newWindowBuilder("imeAppTarget",
+ TYPE_APPLICATION).setWindowToken(mAppWindow.mActivityRecord).build();
mDisplayContent.setImeInputTarget(imeAppTarget);
mDisplayContent.setImeLayeringTarget(imeAppTarget);
mDisplayContent.setImeControlTarget(imeAppTarget);
// Set a popup IME layering target and keeps the original IME control target behinds it.
- final WindowState popupImeTargetWin = createWindow(imeAppTarget,
- TYPE_APPLICATION_SUB_PANEL, mAppWindow.mActivityRecord, "popupImeTargetWin");
+ final WindowState popupImeTargetWin = newWindowBuilder("popupImeTargetWin",
+ TYPE_APPLICATION_SUB_PANEL).setParent(imeAppTarget).setWindowToken(
+ mAppWindow.mActivityRecord).build();
mDisplayContent.setImeLayeringTarget(popupImeTargetWin);
mDisplayContent.updateImeParent();
@@ -424,11 +425,11 @@ public class ZOrderingTests extends WindowTestsBase {
// then we can drop all negative layering on the windowing side.
final WindowState anyWindow = createWindow("anyWindow");
- final WindowState child = createWindow(anyWindow, TYPE_APPLICATION_MEDIA, mDisplayContent,
- "TypeApplicationMediaChild");
- final WindowState mediaOverlayChild = createWindow(anyWindow,
- TYPE_APPLICATION_MEDIA_OVERLAY,
- mDisplayContent, "TypeApplicationMediaOverlayChild");
+ final WindowState child = newWindowBuilder("TypeApplicationMediaChild",
+ TYPE_APPLICATION_MEDIA).setParent(anyWindow).setDisplay(mDisplayContent).build();
+ final WindowState mediaOverlayChild = newWindowBuilder("TypeApplicationMediaOverlayChild",
+ TYPE_APPLICATION_MEDIA_OVERLAY).setParent(anyWindow).setDisplay(
+ mDisplayContent).build();
mDisplayContent.assignChildLayers(mTransaction);
@@ -440,14 +441,17 @@ public class ZOrderingTests extends WindowTestsBase {
public void testAssignWindowLayers_ForPostivelyZOrderedSubtype() {
final WindowState anyWindow = createWindow("anyWindow");
final ArrayList<WindowState> childList = new ArrayList<>();
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_PANEL, mDisplayContent,
- "TypeApplicationPanelChild"));
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_SUB_PANEL, mDisplayContent,
- "TypeApplicationSubPanelChild"));
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_ATTACHED_DIALOG, mDisplayContent,
- "TypeApplicationAttachedDialogChild"));
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_ABOVE_SUB_PANEL, mDisplayContent,
- "TypeApplicationAboveSubPanelPanelChild"));
+ childList.add(newWindowBuilder("TypeApplicationPanelChild",
+ TYPE_APPLICATION_PANEL).setParent(anyWindow).setDisplay(mDisplayContent).build());
+ childList.add(newWindowBuilder("TypeApplicationSubPanelChild",
+ TYPE_APPLICATION_SUB_PANEL).setParent(anyWindow).setDisplay(
+ mDisplayContent).build());
+ childList.add(newWindowBuilder("TypeApplicationAttachedDialogChild",
+ TYPE_APPLICATION_ATTACHED_DIALOG).setParent(anyWindow).setDisplay(
+ mDisplayContent).build());
+ childList.add(newWindowBuilder("TypeApplicationAboveSubPanelPanelChild",
+ TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(anyWindow).setDisplay(
+ mDisplayContent).build());
final LayerRecordingTransaction t = mTransaction;
mDisplayContent.assignChildLayers(t);
@@ -469,8 +473,8 @@ public class ZOrderingTests extends WindowTestsBase {
// Create a popupWindow
assertWindowHigher(mImeWindow, mAppWindow);
- final WindowState popupWindow = createWindow(mAppWindow, TYPE_APPLICATION_PANEL,
- mDisplayContent, "PopupWindow");
+ final WindowState popupWindow = newWindowBuilder("PopupWindow",
+ TYPE_APPLICATION_PANEL).setParent(mAppWindow).setDisplay(mDisplayContent).build();
spyOn(popupWindow);
mDisplayContent.assignChildLayers(mTransaction);
@@ -492,8 +496,9 @@ public class ZOrderingTests extends WindowTestsBase {
makeWindowVisible(mImeWindow);
// Create a popupWindow
- final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
- mDisplayContent, "SystemDialog", true);
+ final WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+ TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
spyOn(systemDialogWindow);
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index 71f303311047..cadb0bdb41e1 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -72,6 +72,7 @@ android_ravenwood_test {
"tests/**/*.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
// Make the current.txt available for use by the cts/tests/signature and /vendor tests.
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
index df92898d76b1..9640a84eb9ca 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
@@ -29,6 +29,7 @@ public class JankUtils {
AppJankStats jankStats = new AppJankStats(
/*App Uid*/APP_ID,
/*Widget Id*/"test widget id",
+ /*navigationComponent*/null,
/*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL,
/*Widget State*/AppJankStats.WIDGET_STATE_SCROLLING,
/*Total Frames*/100,
diff --git a/tests/InputScreenshotTest/robotests/Android.bp b/tests/InputScreenshotTest/robotests/Android.bp
index b2414a85c095..63a13849ee7f 100644
--- a/tests/InputScreenshotTest/robotests/Android.bp
+++ b/tests/InputScreenshotTest/robotests/Android.bp
@@ -66,7 +66,6 @@ android_robolectric_test {
"android.test.mock.stubs.system",
"truth",
],
- upstream: true,
java_resource_dirs: ["config"],
instrumentation_for: "InputRoboApp",
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 9f35c7b7fa33..e294da101fb7 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -65,6 +65,7 @@ android_ravenwood_test {
"src/com/android/internal/util/ParcellingTests.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
java_test_helper_library {
diff --git a/tests/testables/tests/AndroidTest.xml b/tests/testables/tests/AndroidTest.xml
index 85f6e6257770..392bf67cb13b 100644
--- a/tests/testables/tests/AndroidTest.xml
+++ b/tests/testables/tests/AndroidTest.xml
@@ -45,6 +45,7 @@
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
<option name="directory-keys" value="/data/user/0/com.android.testables/files"/>
+ <option name="directory-keys" value="/data/user/10/com.android.testables/files"/>
<option name="collect-on-run-ended-only" value="true"/>
<option name="clean-up" value="true"/>
</metrics_collector>