summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java8
-rw-r--r--core/java/android/app/ActivityThread.java4
-rw-r--r--core/java/android/app/AppCompatTaskInfo.java22
-rw-r--r--core/java/android/app/notification.aconfig10
-rw-r--r--core/java/android/os/LegacyMessageQueue/MessageQueue.java8
-rw-r--r--core/java/android/os/TestLooperManager.java10
-rw-r--r--core/java/android/service/dreams/DreamService.java101
-rw-r--r--core/java/com/android/internal/policy/DesktopModeCompatUtils.java60
-rw-r--r--core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java2
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java99
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/Operations.java7
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java14
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java17
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java9
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java11
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java11
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java5
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java314
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java2
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java146
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java13
-rw-r--r--core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java11
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java21
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java2
-rw-r--r--core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java6
-rw-r--r--core/jni/android_view_InputChannel.cpp29
-rw-r--r--libs/WindowManager/Shell/aconfig/OWNERS3
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig9
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt8
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml6
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml5
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt163
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt31
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt98
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt30
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt97
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt57
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java10
-rw-r--r--location/java/android/location/GnssClock.java2
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig10
-rw-r--r--media/java/android/media/quality/Android.bp27
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl2
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl2
-rw-r--r--media/java/android/media/quality/include/quality/MediaQualityManager.h127
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt10
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt288
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt395
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt44
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt211
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt118
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt134
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt80
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt7
-rw-r--r--packages/SystemUI/res/values/config.xml2
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/res/values/tiles_states_strings.xml10
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java50
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java255
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt135
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt113
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java29
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java383
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java48
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java6
-rw-r--r--services/core/java/com/android/server/am/BroadcastConstants.java18
-rw-r--r--services/core/java/com/android/server/am/BroadcastProcessQueue.java68
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueueImpl.java39
-rw-r--r--services/core/java/com/android/server/am/ProcessRecord.java11
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java3
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java51
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerShellCommand.java14
-rw-r--r--services/core/java/com/android/server/security/CertificateRevocationStatusManager.java5
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java2
-rw-r--r--services/core/java/com/android/server/wm/AppCompatUtils.java1
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java50
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java2
-rw-r--r--services/core/java/com/android/server/wm/LaunchParamsController.java14
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java37
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp9
-rw-r--r--services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java1
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java37
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java38
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java12
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java26
-rw-r--r--services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java36
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java35
-rw-r--r--tests/Input/assets/testPointerScale.pngbin949 -> 947 bytes
-rw-r--r--tests/testables/src/android/testing/TestableLooper.java30
196 files changed, 4547 insertions, 771 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 251776e907d8..44e4999ccf44 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -5369,7 +5369,9 @@ public class AlarmManagerService extends SystemService {
// to do any wakelock or stats tracking, so we have nothing
// left to do here but go on to the next thing.
mSendFinishCount++;
- if (Flags.acquireWakelockBeforeSend()) {
+ if (Flags.acquireWakelockBeforeSend() && mBroadcastRefCount == 0) {
+ // No other alarms are in-flight and this dispatch failed. We will
+ // acquire the wakelock again before the next dispatch.
mWakeLock.release();
}
return;
@@ -5409,7 +5411,9 @@ public class AlarmManagerService extends SystemService {
// stats management to do. It threw before we posted the delayed
// timeout message, so we're done here.
mListenerFinishCount++;
- if (Flags.acquireWakelockBeforeSend()) {
+ if (Flags.acquireWakelockBeforeSend() && mBroadcastRefCount == 0) {
+ // No other alarms are in-flight and this dispatch failed. We will
+ // acquire the wakelock again before the next dispatch.
mWakeLock.release();
}
return;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index e193d2ddd5a7..0a2b1eaaad6b 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4002,6 +4002,10 @@ public final class ActivityThread extends ClientTransactionHandler
+ (fromIpc ? " (from ipc" : ""));
}
}
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "updateProcessState: processState=" + processState);
+ }
}
/** Converts a process state to a VM process state. */
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index ea4646aa9eb9..3fd9d8b26611 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -104,6 +104,8 @@ public class AppCompatTaskInfo implements Parcelable {
public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9;
/** Top activity flag for whether restart menu is shown due to display move. */
private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10;
+ /** Top activity flag for whether activity opted out of edge to edge. */
+ public static final int FLAG_OPT_OUT_EDGE_TO_EDGE = FLAG_BASE << 11;
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
@@ -118,7 +120,8 @@ public class AppCompatTaskInfo implements Parcelable {
FLAG_FULLSCREEN_OVERRIDE_SYSTEM,
FLAG_FULLSCREEN_OVERRIDE_USER,
FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE,
- FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE
+ FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE,
+ FLAG_OPT_OUT_EDGE_TO_EDGE
})
public @interface TopActivityFlag {}
@@ -132,7 +135,8 @@ public class AppCompatTaskInfo implements Parcelable {
@TopActivityFlag
private static final int FLAGS_ORGANIZER_INTERESTED = FLAG_IS_FROM_LETTERBOX_DOUBLE_TAP
| FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON | FLAG_FULLSCREEN_OVERRIDE_SYSTEM
- | FLAG_FULLSCREEN_OVERRIDE_USER | FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE;
+ | FLAG_FULLSCREEN_OVERRIDE_USER | FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE
+ | FLAG_OPT_OUT_EDGE_TO_EDGE;
@TopActivityFlag
private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED
@@ -347,6 +351,20 @@ public class AppCompatTaskInfo implements Parcelable {
setTopActivityFlag(FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE, enable);
}
+ /**
+ * Sets the top activity flag for whether the activity has opted out of edge to edge.
+ */
+ public void setOptOutEdgeToEdge(boolean enable) {
+ setTopActivityFlag(FLAG_OPT_OUT_EDGE_TO_EDGE, enable);
+ }
+
+ /**
+ * @return {@code true} if the top activity has opted out of edge to edge.
+ */
+ public boolean hasOptOutEdgeToEdge() {
+ return isTopActivityFlagEnabled(FLAG_OPT_OUT_EDGE_TO_EDGE);
+ }
+
/** Clear all top activity flags and set to false. */
public void clearTopActivityFlags() {
mTopActivityFlags = FLAG_UNDEFINED;
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 4afe75f7814c..3eaf2c40daca 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -63,6 +63,16 @@ flag {
}
flag {
+ name: "modes_ui_dnd_tile"
+ namespace: "systemui"
+ description: "Shows a dedicated tile for the DND mode; dependent on modes_ui"
+ bug: "401217520"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "modes_hsum"
namespace: "systemui"
description: "Fixes for modes (and DND/Zen in general) with HSUM or secondary users"
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index 132bdd1e56b8..1cf57de08d5f 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -786,8 +786,8 @@ public final class MessageQueue {
}
/**
- * Get the timestamp of the next executable message in our priority queue.
- * Returns null if there are no messages ready for delivery.
+ * Get the timestamp of the next message in our priority queue.
+ * Returns null if there are no messages in the queue.
*
* Caller must ensure that this doesn't race 'next' from the Looper thread.
*/
@@ -799,8 +799,8 @@ public final class MessageQueue {
}
/**
- * Return the next executable message in our priority queue.
- * Returns null if there are no messages ready for delivery
+ * Return the next message in our priority queue.
+ * Returns null if there are no messages in the queue.
*
* Caller must ensure that this doesn't race 'next' from the Looper thread.
*/
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index 1a54f4df58fb..204e3444c547 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -96,8 +96,8 @@ public class TestLooperManager {
}
/**
- * Retrieves and removes the next message that should be executed by this queue.
- * If the queue is empty or no messages are deliverable, returns null.
+ * Retrieves and removes the next message in this queue.
+ * If the queue is empty, returns null.
* This method never blocks.
*
* <p>Callers should always call {@link #recycle(Message)} on the message when all interactions
@@ -112,9 +112,9 @@ public class TestLooperManager {
}
/**
- * Retrieves, but does not remove, the values of {@link Message#when} of next message that
- * should be executed by this queue.
- * If the queue is empty or no messages are deliverable, returns null.
+ * Retrieves, but does not remove, the values of {@link Message#when} of next message in the
+ * queue.
+ * If the queue is empty, returns null.
* This method never blocks.
*/
@FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY)
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index ce31e1ea7e38..df3b8baa40c8 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -455,7 +455,7 @@ public class DreamService extends Service implements Window.Callback {
// Simply wake up in the case the device is not locked.
if (!keyguardManager.isKeyguardLocked()) {
- wakeUp();
+ wakeUp(false);
return true;
}
@@ -477,11 +477,11 @@ public class DreamService extends Service implements Window.Callback {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on keyEvent");
- wakeUp();
+ wakeUp(false);
return true;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (mDebug) Slog.v(mTag, "Waking up on back key");
- wakeUp();
+ wakeUp(false);
return true;
}
return mWindow.superDispatchKeyEvent(event);
@@ -492,7 +492,7 @@ public class DreamService extends Service implements Window.Callback {
public boolean dispatchKeyShortcutEvent(KeyEvent event) {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on keyShortcutEvent");
- wakeUp();
+ wakeUp(false);
return true;
}
return mWindow.superDispatchKeyShortcutEvent(event);
@@ -505,7 +505,7 @@ public class DreamService extends Service implements Window.Callback {
// but finish()es on any other kind of activity
if (!mInteractive && event.getActionMasked() == MotionEvent.ACTION_UP) {
if (mDebug) Slog.v(mTag, "Waking up on touchEvent");
- wakeUp();
+ wakeUp(false);
return true;
}
return mWindow.superDispatchTouchEvent(event);
@@ -516,7 +516,7 @@ public class DreamService extends Service implements Window.Callback {
public boolean dispatchTrackballEvent(MotionEvent event) {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on trackballEvent");
- wakeUp();
+ wakeUp(false);
return true;
}
return mWindow.superDispatchTrackballEvent(event);
@@ -527,7 +527,7 @@ public class DreamService extends Service implements Window.Callback {
public boolean dispatchGenericMotionEvent(MotionEvent event) {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on genericMotionEvent");
- wakeUp();
+ wakeUp(false);
return true;
}
return mWindow.superDispatchGenericMotionEvent(event);
@@ -925,32 +925,37 @@ public class DreamService extends Service implements Window.Callback {
}
}
- private synchronized void updateDoze() {
- if (mDreamToken == null) {
- Slog.w(mTag, "Updating doze without a dream token.");
- return;
- }
+ /**
+ * Updates doze state. Note that this must be called on the mHandler.
+ */
+ private void updateDoze() {
+ mHandler.post(() -> {
+ if (mDreamToken == null) {
+ Slog.w(mTag, "Updating doze without a dream token.");
+ return;
+ }
- if (mDozing) {
- try {
- Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState
- + " mDozeScreenBrightness=" + mDozeScreenBrightness
- + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat);
- if (startAndStopDozingInBackground()) {
- mDreamManager.startDozingOneway(
- mDreamToken, mDozeScreenState, mDozeScreenStateReason,
- mDozeScreenBrightnessFloat, mDozeScreenBrightness,
- mUseNormalBrightnessForDoze);
- } else {
- mDreamManager.startDozing(
- mDreamToken, mDozeScreenState, mDozeScreenStateReason,
- mDozeScreenBrightnessFloat, mDozeScreenBrightness,
- mUseNormalBrightnessForDoze);
+ if (mDozing) {
+ try {
+ Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState
+ + " mDozeScreenBrightness=" + mDozeScreenBrightness
+ + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat);
+ if (startAndStopDozingInBackground()) {
+ mDreamManager.startDozingOneway(
+ mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+ mDozeScreenBrightnessFloat, mDozeScreenBrightness,
+ mUseNormalBrightnessForDoze);
+ } else {
+ mDreamManager.startDozing(
+ mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+ mDozeScreenBrightnessFloat, mDozeScreenBrightness,
+ mUseNormalBrightnessForDoze);
+ }
+ } catch (RemoteException ex) {
+ // system server died
}
- } catch (RemoteException ex) {
- // system server died
}
- }
+ });
}
/**
@@ -966,14 +971,20 @@ public class DreamService extends Service implements Window.Callback {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public void stopDozing() {
- if (mDozing) {
- mDozing = false;
- try {
- mDreamManager.stopDozing(mDreamToken);
- } catch (RemoteException ex) {
- // system server died
+ mHandler.post(() -> {
+ if (mDreamToken == null) {
+ return;
}
- }
+
+ if (mDozing) {
+ mDozing = false;
+ try {
+ mDreamManager.stopDozing(mDreamToken);
+ } catch (RemoteException ex) {
+ // system server died
+ }
+ }
+ });
}
/**
@@ -1201,7 +1212,7 @@ public class DreamService extends Service implements Window.Callback {
@Override
public void onExitRequested() {
// Simply finish dream when exit is requested.
- mHandler.post(() -> finish());
+ mHandler.post(() -> finishInternal());
}
@Override
@@ -1299,9 +1310,13 @@ public class DreamService extends Service implements Window.Callback {
* </p>
*/
public final void finish() {
+ mHandler.post(this::finishInternal);
+ }
+
+ private void finishInternal() {
// If there is an active overlay connection, signal that the dream is ending before
- // continuing. Note that the overlay cannot rely on the unbound state, since another dream
- // might have bound to it in the meantime.
+ // continuing. Note that the overlay cannot rely on the unbound state, since another
+ // dream might have bound to it in the meantime.
if (mOverlayConnection != null) {
mOverlayConnection.addConsumer(overlay -> {
try {
@@ -1357,7 +1372,7 @@ public class DreamService extends Service implements Window.Callback {
* </p>
*/
public final void wakeUp() {
- wakeUp(false);
+ mHandler.post(()-> wakeUp(false));
}
/**
@@ -1559,7 +1574,7 @@ public class DreamService extends Service implements Window.Callback {
if (mActivity != null && !mActivity.isFinishing()) {
mActivity.finishAndRemoveTask();
} else {
- finish();
+ finishInternal();
}
mDreamToken = null;
@@ -1719,7 +1734,7 @@ public class DreamService extends Service implements Window.Callback {
// the window reference in order to fully release the DreamActivity.
mWindow = null;
mActivity = null;
- finish();
+ finishInternal();
}
if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) {
diff --git a/core/java/com/android/internal/policy/DesktopModeCompatUtils.java b/core/java/com/android/internal/policy/DesktopModeCompatUtils.java
new file mode 100644
index 000000000000..d7cfbdfed99c
--- /dev/null
+++ b/core/java/com/android/internal/policy/DesktopModeCompatUtils.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.policy;
+
+import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED;
+import static android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS;
+
+import android.annotation.NonNull;
+import android.content.pm.ActivityInfo;
+import android.window.DesktopModeFlags;
+
+/**
+ * Utility functions for app compat in desktop windowing used by both window manager and System UI.
+ * @hide
+ */
+public final class DesktopModeCompatUtils {
+
+ /**
+ * Whether the caption insets should be excluded from configuration for system to handle.
+ * The caller should ensure the activity is in or entering desktop view.
+ *
+ * <p> The treatment is enabled when all the of the following is true:
+ * <li> Any flags to forcibly consume caption insets are enabled.
+ * <li> Top activity have configuration coupled with insets.
+ * <li> Task is not resizeable or per-app override
+ * {@link ActivityInfo#OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS} is enabled.
+ */
+ public static boolean shouldExcludeCaptionFromAppBounds(@NonNull ActivityInfo info,
+ boolean isResizeable, boolean optOutEdgeToEdge) {
+ return DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue()
+ && isAnyForceConsumptionFlagsEnabled()
+ && !isConfigurationDecoupled(info, optOutEdgeToEdge)
+ && (!isResizeable
+ || info.isChangeEnabled(OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS));
+ }
+
+ private static boolean isConfigurationDecoupled(@NonNull ActivityInfo info,
+ boolean optOutEdgeToEdge) {
+ return info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED) && !optOutEdgeToEdge;
+ }
+
+ private static boolean isAnyForceConsumptionFlagsEnabled() {
+ return DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue()
+ || DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue();
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java
index d3a496db2ca9..f70f4cbceb70 100644
--- a/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java
+++ b/core/java/com/android/internal/widget/remotecompose/accessibility/CoreDocumentAccessibility.java
@@ -179,7 +179,7 @@ public class CoreDocumentAccessibility implements RemoteComposeDocumentAccessibi
* @return
*/
public boolean performClick(Component component) {
- mDocument.performClick(mRemoteContext, component.getComponentId());
+ mDocument.performClick(mRemoteContext, component.getComponentId(), "");
return true;
}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 3cc998576741..caf19e1ed34a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -19,8 +19,10 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
import com.android.internal.widget.remotecompose.core.operations.ComponentValue;
import com.android.internal.widget.remotecompose.core.operations.DrawContent;
+import com.android.internal.widget.remotecompose.core.operations.FloatConstant;
import com.android.internal.widget.remotecompose.core.operations.FloatExpression;
import com.android.internal.widget.remotecompose.core.operations.Header;
import com.android.internal.widget.remotecompose.core.operations.IntegerExpression;
@@ -29,6 +31,7 @@ import com.android.internal.widget.remotecompose.core.operations.RootContentBeha
import com.android.internal.widget.remotecompose.core.operations.ShaderData;
import com.android.internal.widget.remotecompose.core.operations.TextData;
import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.Utils;
import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations;
import com.android.internal.widget.remotecompose.core.operations.layout.Component;
import com.android.internal.widget.remotecompose.core.operations.layout.Container;
@@ -42,6 +45,8 @@ import com.android.internal.widget.remotecompose.core.operations.utilities.IntMa
import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
import com.android.internal.widget.remotecompose.core.serialize.Serializable;
+import com.android.internal.widget.remotecompose.core.types.IntegerConstant;
+import com.android.internal.widget.remotecompose.core.types.LongConstant;
import java.util.ArrayList;
import java.util.HashMap;
@@ -68,7 +73,7 @@ public class CoreDocument implements Serializable {
// We also keep a more fine-grained BUILD number, exposed as
// ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD
- static final float BUILD = 0.4f;
+ static final float BUILD = 0.5f;
@NonNull ArrayList<Operation> mOperations = new ArrayList<>();
@@ -442,6 +447,94 @@ public class CoreDocument implements Serializable {
return mDocProperties.get(key);
}
+ /**
+ * Apply a collection of operations to the document
+ *
+ * @param delta the delta to apply
+ */
+ public void applyUpdate(CoreDocument delta) {
+ HashMap<Integer, TextData> txtData = new HashMap<Integer, TextData>();
+ HashMap<Integer, BitmapData> imgData = new HashMap<Integer, BitmapData>();
+ HashMap<Integer, FloatConstant> fltData = new HashMap<Integer, FloatConstant>();
+ HashMap<Integer, IntegerConstant> intData = new HashMap<Integer, IntegerConstant>();
+ HashMap<Integer, LongConstant> longData = new HashMap<Integer, LongConstant>();
+ recursiveTreverse(
+ mOperations,
+ (op) -> {
+ if (op instanceof TextData) {
+ TextData d = (TextData) op;
+ txtData.put(d.mTextId, d);
+ } else if (op instanceof BitmapData) {
+ BitmapData d = (BitmapData) op;
+ imgData.put(d.mImageId, d);
+ } else if (op instanceof FloatConstant) {
+ FloatConstant d = (FloatConstant) op;
+ fltData.put(d.mId, d);
+ } else if (op instanceof IntegerConstant) {
+ IntegerConstant d = (IntegerConstant) op;
+ intData.put(d.mId, d);
+ } else if (op instanceof LongConstant) {
+ LongConstant d = (LongConstant) op;
+ longData.put(d.mId, d);
+ }
+ });
+
+ recursiveTreverse(
+ delta.mOperations,
+ (op) -> {
+ if (op instanceof TextData) {
+ TextData t = (TextData) op;
+ TextData txtInDoc = txtData.get(t.mTextId);
+ if (txtInDoc != null) {
+ txtInDoc.update(t);
+ Utils.log("update" + t.mText);
+ txtInDoc.markDirty();
+ }
+ } else if (op instanceof BitmapData) {
+ BitmapData b = (BitmapData) op;
+ BitmapData imgInDoc = imgData.get(b.mImageId);
+ if (imgInDoc != null) {
+ imgInDoc.update(b);
+ imgInDoc.markDirty();
+ }
+ } else if (op instanceof FloatConstant) {
+ FloatConstant f = (FloatConstant) op;
+ FloatConstant fltInDoc = fltData.get(f.mId);
+ if (fltInDoc != null) {
+ fltInDoc.update(f);
+ fltInDoc.markDirty();
+ }
+ } else if (op instanceof IntegerConstant) {
+ IntegerConstant ic = (IntegerConstant) op;
+ IntegerConstant intInDoc = intData.get(ic.mId);
+ if (intInDoc != null) {
+ intInDoc.update(ic);
+ intInDoc.markDirty();
+ }
+ } else if (op instanceof LongConstant) {
+ LongConstant lc = (LongConstant) op;
+ LongConstant longInDoc = longData.get(lc.mId);
+ if (longInDoc != null) {
+ longInDoc.update(lc);
+ longInDoc.markDirty();
+ }
+ }
+ });
+ }
+
+ private interface Visitor {
+ void visit(Operation op);
+ }
+
+ private void recursiveTreverse(ArrayList<Operation> mOperations, Visitor visitor) {
+ for (Operation op : mOperations) {
+ if (op instanceof Container) {
+ recursiveTreverse(((Component) op).mList, visitor);
+ }
+ visitor.visit(op);
+ }
+ }
+
// ============== Haptic support ==================
public interface HapticEngine {
/**
@@ -911,7 +1004,7 @@ public class CoreDocument implements Serializable {
*
* @param id the click area id
*/
- public void performClick(@NonNull RemoteContext context, int id) {
+ public void performClick(@NonNull RemoteContext context, int id, @NonNull String metadata) {
for (ClickAreaRepresentation clickArea : mClickAreas) {
if (clickArea.mId == id) {
warnClickListeners(clickArea);
@@ -920,7 +1013,7 @@ public class CoreDocument implements Serializable {
}
for (IdActionCallback listener : mIdActionListeners) {
- listener.onAction(id, "");
+ listener.onAction(id, metadata);
}
Component component = getComponent(id);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index 20897ba77de4..ac9f98bd6b15 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -104,6 +104,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.managers
import com.android.internal.widget.remotecompose.core.operations.layout.managers.CollapsibleRowLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.FitBoxLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.ImageLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.StateLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.TextLayout;
@@ -115,6 +116,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.modifier
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.GraphicsLayerModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HostActionMetadataOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HostActionOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HostNamedActionOperation;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.MarqueeModifierOperation;
@@ -247,6 +249,7 @@ public class Operations {
public static final int LAYOUT_CANVAS_CONTENT = 207;
public static final int LAYOUT_TEXT = 208;
public static final int LAYOUT_STATE = 217;
+ public static final int LAYOUT_IMAGE = 234;
public static final int COMPONENT_START = 2;
@@ -278,6 +281,7 @@ public class Operations {
public static final int MODIFIER_VISIBILITY = 211;
public static final int HOST_ACTION = 209;
+ public static final int HOST_METADATA_ACTION = 216;
public static final int HOST_NAMED_ACTION = 210;
public static final int VALUE_INTEGER_CHANGE_ACTION = 212;
@@ -385,6 +389,7 @@ public class Operations {
map.put(CONTAINER_END, ContainerEnd::read);
map.put(HOST_ACTION, HostActionOperation::read);
+ map.put(HOST_METADATA_ACTION, HostActionMetadataOperation::read);
map.put(HOST_NAMED_ACTION, HostNamedActionOperation::read);
map.put(VALUE_INTEGER_CHANGE_ACTION, ValueIntegerChangeActionOperation::read);
map.put(
@@ -407,7 +412,7 @@ public class Operations {
map.put(LAYOUT_CANVAS, CanvasLayout::read);
map.put(LAYOUT_CANVAS_CONTENT, CanvasContent::read);
map.put(LAYOUT_TEXT, TextLayout::read);
-
+ map.put(LAYOUT_IMAGE, ImageLayout::read);
map.put(LAYOUT_STATE, StateLayout::read);
map.put(DRAW_CONTENT, DrawContent::read);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index 161a5f17ef23..1f026687680f 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -102,6 +102,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.managers
import com.android.internal.widget.remotecompose.core.operations.layout.managers.CollapsibleRowLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.ColumnLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.FitBoxLayout;
+import com.android.internal.widget.remotecompose.core.operations.layout.managers.ImageLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.RowLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.StateLayout;
import com.android.internal.widget.remotecompose.core.operations.layout.managers.TextLayout;
@@ -2094,6 +2095,19 @@ public class RemoteComposeBuffer {
}
/**
+ * Add an imagelayout command
+ *
+ * @param componentId component id
+ * @param animationId animation id
+ * @param bitmapId bitmap id
+ */
+ public void addImage(
+ int componentId, int animationId, int bitmapId, int scaleType, float alpha) {
+ mLastComponentId = getComponentId(componentId);
+ ImageLayout.apply(mBuffer, componentId, animationId, bitmapId, scaleType, alpha);
+ }
+
+ /**
* Add a row start tag
*
* @param componentId component id
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
index 255d7a46e49e..90929e06ecd0 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/BitmapData.java
@@ -41,12 +41,12 @@ import java.util.List;
public class BitmapData extends Operation implements SerializableToString, Serializable {
private static final int OP_CODE = Operations.DATA_BITMAP;
private static final String CLASS_NAME = "BitmapData";
- int mImageId;
+ public final int mImageId;
int mImageWidth;
int mImageHeight;
short mType;
short mEncoding;
- @NonNull final byte[] mBitmap;
+ @NonNull byte[] mBitmap;
/** The max size of width or height */
public static final int MAX_IMAGE_DIMENSION = 8000;
@@ -91,6 +91,19 @@ public class BitmapData extends Operation implements SerializableToString, Seria
}
/**
+ * Update the bitmap data
+ *
+ * @param from the bitmap to copy
+ */
+ public void update(BitmapData from) {
+ this.mImageWidth = from.mImageWidth;
+ this.mImageHeight = from.mImageHeight;
+ this.mBitmap = from.mBitmap;
+ this.mType = from.mType;
+ this.mEncoding = from.mEncoding;
+ }
+
+ /**
* The width of the image
*
* @return the width
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java
index 233e246d3868..5096aaf03c9d 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java
@@ -42,6 +42,15 @@ public class FloatConstant extends Operation implements Serializable {
this.mValue = value;
}
+ /**
+ * Copy the value from another operation
+ *
+ * @param from value to copy from
+ */
+ public void update(FloatConstant from) {
+ mValue = from.mValue;
+ }
+
@Override
public void write(@NonNull WireBuffer buffer) {
apply(buffer, mId, mValue);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
index 67773d1d6187..d8ef4cbba4d6 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java
@@ -37,7 +37,7 @@ public class TextData extends Operation implements SerializableToString, Seriali
private static final int OP_CODE = Operations.DATA_TEXT;
private static final String CLASS_NAME = "TextData";
public final int mTextId;
- @NonNull public final String mText;
+ @NonNull public String mText;
public static final int MAX_STRING_SIZE = 4000;
public TextData(int textId, @NonNull String text) {
@@ -45,6 +45,15 @@ public class TextData extends Operation implements SerializableToString, Seriali
this.mText = text;
}
+ /**
+ * Copy the data from another text data
+ *
+ * @param from source to copy from
+ */
+ public void update(TextData from) {
+ mText = from.mText;
+ }
+
@Override
public void write(@NonNull WireBuffer buffer) {
apply(buffer, mTextId, mText);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
index 76bb96d1b61a..f1158d91f94b 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java
@@ -27,6 +27,7 @@ import com.android.internal.widget.remotecompose.core.SerializableToString;
import com.android.internal.widget.remotecompose.core.TouchListener;
import com.android.internal.widget.remotecompose.core.VariableSupport;
import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
import com.android.internal.widget.remotecompose.core.operations.ComponentValue;
import com.android.internal.widget.remotecompose.core.operations.TextData;
import com.android.internal.widget.remotecompose.core.operations.TouchExpression;
@@ -1131,14 +1132,14 @@ public class Component extends PaintOperation
}
/**
- * Extract child TextData elements
+ * Extract child Data elements
*
- * @param data an ArrayList that will be populated with the TextData elements (if any)
+ * @param data an ArrayList that will be populated with the Data elements (if any)
*/
- public void getData(@NonNull ArrayList<TextData> data) {
+ public void getData(@NonNull ArrayList<Operation> data) {
for (Operation op : mList) {
- if (op instanceof TextData) {
- data.add((TextData) op);
+ if (op instanceof TextData || op instanceof BitmapData) {
+ data.add(op);
}
}
}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
index e57438662012..6163d8099b8c 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java
@@ -30,7 +30,6 @@ import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate;
import com.android.internal.widget.remotecompose.core.operations.PaintData;
-import com.android.internal.widget.remotecompose.core.operations.TextData;
import com.android.internal.widget.remotecompose.core.operations.TouchExpression;
import com.android.internal.widget.remotecompose.core.operations.layout.animation.AnimationSpec;
import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers;
@@ -138,7 +137,7 @@ public class LayoutComponent extends Component {
@Override
public void inflate() {
- ArrayList<TextData> data = new ArrayList<>();
+ ArrayList<Operation> data = new ArrayList<>();
ArrayList<Operation> supportedOperations = new ArrayList<>();
for (Operation op : mList) {
@@ -186,8 +185,6 @@ public class LayoutComponent extends Component {
((ScrollModifierOperation) op).inflate(this);
}
mComponentModifiers.add((ModifierOperation) op);
- } else if (op instanceof TextData) {
- data.add((TextData) op);
} else if (op instanceof TouchExpression
|| (op instanceof PaintData)
|| (op instanceof FloatExpression)) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java
new file mode 100644
index 000000000000..a4ed0c1319d4
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/ImageLayout.java
@@ -0,0 +1,314 @@
+/*
+ * 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.internal.widget.remotecompose.core.operations.layout.managers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
+import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.VariableSupport;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.BitmapData;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
+import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.utilities.ImageScaling;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+
+import java.util.List;
+
+public class ImageLayout extends LayoutManager implements VariableSupport {
+
+ private static final boolean DEBUG = false;
+ private int mBitmapId = -1;
+ private int mScaleType;
+ private float mAlpha = 1f;
+
+ @NonNull ImageScaling mScaling = new ImageScaling();
+ @NonNull PaintBundle mPaint = new PaintBundle();
+
+ @Override
+ public void registerListening(@NonNull RemoteContext context) {
+ if (mBitmapId != -1) {
+ context.listensTo(mBitmapId, this);
+ }
+ }
+
+ public ImageLayout(
+ @Nullable Component parent,
+ int componentId,
+ int animationId,
+ int bitmapId,
+ float x,
+ float y,
+ float width,
+ float height,
+ int scaleType,
+ float alpha) {
+ super(parent, componentId, animationId, x, y, width, height);
+ mBitmapId = bitmapId;
+ mScaleType = scaleType & 0xFF;
+ mAlpha = alpha;
+ }
+
+ public ImageLayout(
+ @Nullable Component parent,
+ int componentId,
+ int animationId,
+ int bitmapId,
+ int scaleType,
+ float alpha) {
+ this(parent, componentId, animationId, bitmapId, 0, 0, 0, 0, scaleType, alpha);
+ }
+
+ @Override
+ public void computeWrapSize(
+ @NonNull PaintContext context,
+ float maxWidth,
+ float maxHeight,
+ boolean horizontalWrap,
+ boolean verticalWrap,
+ @NonNull MeasurePass measure,
+ @NonNull Size size) {
+
+ BitmapData bitmapData = (BitmapData) context.getContext().getObject(mBitmapId);
+ if (bitmapData != null) {
+ size.setWidth(bitmapData.getWidth());
+ size.setHeight(bitmapData.getHeight());
+ }
+ }
+
+ @Override
+ public void computeSize(
+ @NonNull PaintContext context,
+ float minWidth,
+ float maxWidth,
+ float minHeight,
+ float maxHeight,
+ @NonNull MeasurePass measure) {
+ float modifiersWidth = computeModifierDefinedWidth(context.getContext());
+ float modifiersHeight = computeModifierDefinedHeight(context.getContext());
+ ComponentMeasure m = measure.get(this);
+ m.setW(modifiersWidth);
+ m.setH(modifiersHeight);
+ }
+
+ @Override
+ public void paintingComponent(@NonNull PaintContext context) {
+ context.save();
+ context.translate(mX, mY);
+ mComponentModifiers.paint(context);
+ float tx = mPaddingLeft;
+ float ty = mPaddingTop;
+ context.translate(tx, ty);
+ float w = mWidth - mPaddingLeft - mPaddingRight;
+ float h = mHeight - mPaddingTop - mPaddingBottom;
+ context.clipRect(0f, 0f, w, h);
+
+ BitmapData bitmapData = (BitmapData) context.getContext().getObject(mBitmapId);
+ if (bitmapData != null) {
+ mScaling.setup(
+ 0f,
+ 0f,
+ bitmapData.getWidth(),
+ bitmapData.getHeight(),
+ 0f,
+ 0f,
+ w,
+ h,
+ mScaleType,
+ 1f);
+
+ context.savePaint();
+ if (mAlpha == 1f) {
+ context.drawBitmap(
+ mBitmapId,
+ (int) 0f,
+ (int) 0f,
+ (int) bitmapData.getWidth(),
+ (int) bitmapData.getHeight(),
+ (int) mScaling.mFinalDstLeft,
+ (int) mScaling.mFinalDstTop,
+ (int) mScaling.mFinalDstRight,
+ (int) mScaling.mFinalDstBottom,
+ -1);
+ } else {
+ context.savePaint();
+ mPaint.reset();
+ mPaint.setColor(0f, 0f, 0f, mAlpha);
+ context.applyPaint(mPaint);
+ context.drawBitmap(
+ mBitmapId,
+ (int) 0f,
+ (int) 0f,
+ (int) bitmapData.getWidth(),
+ (int) bitmapData.getHeight(),
+ (int) mScaling.mFinalDstLeft,
+ (int) mScaling.mFinalDstTop,
+ (int) mScaling.mFinalDstRight,
+ (int) mScaling.mFinalDstBottom,
+ -1);
+ context.restorePaint();
+ }
+ context.restorePaint();
+ }
+
+ // debugBox(this, context);
+ context.translate(-tx, -ty);
+ context.restore();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "IMAGE_LAYOUT ["
+ + mComponentId
+ + ":"
+ + mAnimationId
+ + "] ("
+ + mX
+ + ", "
+ + mY
+ + " - "
+ + mWidth
+ + " x "
+ + mHeight
+ + ") "
+ + mVisibility;
+ }
+
+ @NonNull
+ @Override
+ protected String getSerializedName() {
+ return "IMAGE_LAYOUT";
+ }
+
+ @Override
+ public void serializeToString(int indent, @NonNull StringSerializer serializer) {
+ serializer.append(
+ indent,
+ getSerializedName()
+ + " ["
+ + mComponentId
+ + ":"
+ + mAnimationId
+ + "] = "
+ + "["
+ + mX
+ + ", "
+ + mY
+ + ", "
+ + mWidth
+ + ", "
+ + mHeight
+ + "] "
+ + mVisibility
+ + " ("
+ + mBitmapId
+ + "\")");
+ }
+
+ /**
+ * The name of the class
+ *
+ * @return the name
+ */
+ @NonNull
+ public static String name() {
+ return "ImageLayout";
+ }
+
+ /**
+ * The OP_CODE for this command
+ *
+ * @return the opcode
+ */
+ public static int id() {
+ return Operations.LAYOUT_IMAGE;
+ }
+
+ /**
+ * Write the operation to the buffer
+ *
+ * @param buffer
+ * @param componentId
+ * @param animationId
+ * @param bitmapId
+ * @param scaleType
+ * @param alpha
+ */
+ public static void apply(
+ @NonNull WireBuffer buffer,
+ int componentId,
+ int animationId,
+ int bitmapId,
+ int scaleType,
+ float alpha) {
+ buffer.start(id());
+ buffer.writeInt(componentId);
+ buffer.writeInt(animationId);
+ buffer.writeInt(bitmapId);
+ buffer.writeInt(scaleType);
+ buffer.writeFloat(alpha);
+ }
+
+ /**
+ * Read this operation and add it to the list of operations
+ *
+ * @param buffer the buffer to read
+ * @param operations the list of operations that will be added to
+ */
+ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
+ int componentId = buffer.readInt();
+ int animationId = buffer.readInt();
+ int bitmapId = buffer.readInt();
+ int scaleType = buffer.readInt();
+ float alpha = buffer.readFloat();
+ operations.add(new ImageLayout(null, componentId, animationId, bitmapId, scaleType, alpha));
+ }
+
+ /**
+ * Populate the documentation with a description of this operation
+ *
+ * @param doc to append the description to.
+ */
+ public static void documentation(@NonNull DocumentationBuilder doc) {
+ doc.operation("Layout Operations", id(), name())
+ .description("Image layout implementation.\n\n")
+ .field(INT, "COMPONENT_ID", "unique id for this component")
+ .field(
+ INT,
+ "ANIMATION_ID",
+ "id used to match components," + " for animation purposes")
+ .field(INT, "BITMAP_ID", "bitmap id")
+ .field(INT, "SCALE_TYPE", "scale type")
+ .field(FLOAT, "ALPHA", "alpha");
+ }
+
+ @Override
+ public void write(@NonNull WireBuffer buffer) {
+ apply(buffer, mComponentId, mAnimationId, mBitmapId, mScaleType, mAlpha);
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
index 656a3c0fca68..b3d76b765143 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java
@@ -250,7 +250,7 @@ public class BorderModifierOperation extends DecoratorModifierOperation {
context.savePaint();
paint.reset();
paint.setColor(mR, mG, mB, mA);
- paint.setStrokeWidth(mBorderWidth);
+ paint.setStrokeWidth(mBorderWidth * context.getContext().getDensity());
paint.setStyle(PaintBundle.STYLE_STROKE);
context.replacePaint(paint);
if (mShapeType == ShapeType.RECTANGLE) {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java
new file mode 100644
index 000000000000..2170efd68a64
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/HostActionMetadataOperation.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.layout.modifiers;
+
+import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
+
+import android.annotation.NonNull;
+
+import com.android.internal.widget.remotecompose.core.CoreDocument;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.SerializableToString;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
+import com.android.internal.widget.remotecompose.core.operations.layout.ActionOperation;
+import com.android.internal.widget.remotecompose.core.operations.layout.Component;
+import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
+import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
+import com.android.internal.widget.remotecompose.core.serialize.Serializable;
+import com.android.internal.widget.remotecompose.core.serialize.SerializeTags;
+
+import java.util.List;
+
+/** Capture a host action information. This can be triggered on eg. a click. */
+public class HostActionMetadataOperation extends Operation
+ implements ActionOperation, SerializableToString, Serializable {
+ private static final int OP_CODE = Operations.HOST_METADATA_ACTION;
+
+ int mActionId = -1;
+ int mMetadataId = -1;
+
+ public HostActionMetadataOperation(int id, int metadataId) {
+ mActionId = id;
+ mMetadataId = metadataId;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "HostMetadataActionOperation(" + mActionId + ":" + mMetadataId + ")";
+ }
+
+ public int getActionId() {
+ return mActionId;
+ }
+
+ /**
+ * Returns the serialized name for this operation
+ *
+ * @return the serialized name
+ */
+ @NonNull
+ public String serializedName() {
+ return "HOST_METADATA_ACTION";
+ }
+
+ @Override
+ public void serializeToString(int indent, @NonNull StringSerializer serializer) {
+ serializer.append(indent, serializedName() + " = " + mActionId + ", " + mMetadataId);
+ }
+
+ @Override
+ public void apply(@NonNull RemoteContext context) {}
+
+ @NonNull
+ @Override
+ public String deepToString(@NonNull String indent) {
+ return (indent != null ? indent : "") + toString();
+ }
+
+ @Override
+ public void write(@NonNull WireBuffer buffer) {}
+
+ @Override
+ public void runAction(
+ @NonNull RemoteContext context,
+ @NonNull CoreDocument document,
+ @NonNull Component component,
+ float x,
+ float y) {
+ String metadata = context.getText(mMetadataId);
+ if (metadata == null) {
+ metadata = "";
+ }
+ context.runAction(mActionId, metadata);
+ }
+
+ /**
+ * Write the operation to the buffer
+ *
+ * @param buffer a WireBuffer
+ * @param actionId the action id
+ */
+ public static void apply(@NonNull WireBuffer buffer, int actionId, int metadataId) {
+ buffer.start(OP_CODE);
+ buffer.writeInt(actionId);
+ buffer.writeInt(metadataId);
+ }
+
+ /**
+ * Read this operation and add it to the list of operations
+ *
+ * @param buffer the buffer to read
+ * @param operations the list of operations that will be added to
+ */
+ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
+ int actionId = buffer.readInt();
+ int metadataId = buffer.readInt();
+ operations.add(new HostActionMetadataOperation(actionId, metadataId));
+ }
+
+ /**
+ * Populate the documentation with a description of this operation
+ *
+ * @param doc to append the description to.
+ */
+ public static void documentation(@NonNull DocumentationBuilder doc) {
+ doc.operation("Layout Operations", OP_CODE, "HostAction")
+ .description("Host action. This operation represents a host action")
+ .field(INT, "ACTION_ID", "Host Action ID")
+ .field(INT, "METADATA", "Host Action Text Metadata ID");
+ }
+
+ @Override
+ public void serialize(MapSerializer serializer) {
+ serializer
+ .addTags(SerializeTags.MODIFIER)
+ .addType("HostActionOperation")
+ .add("id", mActionId)
+ .add("metadata", mMetadataId);
+ }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
index bdc765968387..25dcb67fe9e2 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java
@@ -34,14 +34,23 @@ import java.util.List;
public class IntegerConstant extends Operation implements Serializable {
private static final String CLASS_NAME = "IntegerConstant";
- private final int mValue;
- private final int mId;
+ private int mValue;
+ public final int mId;
IntegerConstant(int id, int value) {
mId = id;
mValue = value;
}
+ /**
+ * Updates the value of the integer constant
+ *
+ * @param ic the integer constant to copy
+ */
+ public void update(IntegerConstant ic) {
+ mValue = ic.mValue;
+ }
+
@Override
public void write(@NonNull WireBuffer buffer) {
apply(buffer, mId, mValue);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java
index d071e0a21d22..ab0f7352182a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java
@@ -36,7 +36,7 @@ public class LongConstant extends Operation implements Serializable {
private static final int OP_CODE = Operations.DATA_LONG;
private long mValue;
- private final int mId;
+ public final int mId;
/**
* @param id the id of the constant
@@ -48,6 +48,15 @@ public class LongConstant extends Operation implements Serializable {
}
/**
+ * Copy the value from another longConstant
+ *
+ * @param from the constant to copy from
+ */
+ public void update(LongConstant from) {
+ mValue = from.mValue;
+ }
+
+ /**
* Get the value of the long constant
*
* @return the value of the long
diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
index 1f9a27429067..f5b2cca15e43 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java
@@ -41,6 +41,7 @@ import com.android.internal.widget.remotecompose.core.RemoteContext;
import com.android.internal.widget.remotecompose.core.RemoteContextAware;
import com.android.internal.widget.remotecompose.core.operations.NamedVariable;
import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
+import com.android.internal.widget.remotecompose.player.platform.AndroidRemoteContext;
import com.android.internal.widget.remotecompose.player.platform.RemoteComposeCanvas;
/** A view to to display and play RemoteCompose documents */
@@ -114,6 +115,23 @@ public class RemoteComposePlayer extends FrameLayout implements RemoteContextAwa
return mInner.getDocument();
}
+ /**
+ * This will update values in the already loaded document.
+ *
+ * @param value the document to update variables in the current document width
+ */
+ public void updateDocument(RemoteComposeDocument value) {
+ RemoteComposeDocument document = value;
+ AndroidRemoteContext tmpContext = new AndroidRemoteContext();
+ document.initializeContext(tmpContext);
+ float density = getContext().getResources().getDisplayMetrics().density;
+ tmpContext.setAnimationEnabled(true);
+ tmpContext.setDensity(density);
+ tmpContext.setUseChoreographer(false);
+ mInner.getDocument().mDocument.applyUpdate(document.mDocument);
+ mInner.invalidate();
+ }
+
public void setDocument(RemoteComposeDocument value) {
if (value != null) {
if (value.canBeDisplayed(
@@ -312,7 +330,8 @@ public class RemoteComposePlayer extends FrameLayout implements RemoteContextAwa
}
/**
- * Add a callback for handling id actions events on the document
+ * Add a callback for handling id actions events on the document. Can only be added after the
+ * document has been loaded.
*
* @param callback the callback lambda that will be used when a action is executed
* <p>The parameter of the callback are:
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
index ad2414974780..e1f2924021a4 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -197,7 +197,7 @@ public class AndroidRemoteContext extends RemoteContext {
@Override
public void runAction(int id, @NonNull String metadata) {
- mDocument.performClick(this, id);
+ mDocument.performClick(this, id, metadata);
}
@Override
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
index 29cd40def562..0bc99abc17bc 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java
@@ -154,7 +154,11 @@ public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachSta
param.leftMargin = (int) area.getLeft();
param.topMargin = (int) area.getTop();
viewArea.setOnClickListener(
- view1 -> mDocument.getDocument().performClick(mARContext, area.getId()));
+ view1 ->
+ mDocument
+ .getDocument()
+ .performClick(
+ mARContext, area.getId(), area.getMetadata()));
addView(viewArea, param);
}
if (!clickAreas.isEmpty()) {
diff --git a/core/jni/android_view_InputChannel.cpp b/core/jni/android_view_InputChannel.cpp
index e874163943b6..a6a748c3deb7 100644
--- a/core/jni/android_view_InputChannel.cpp
+++ b/core/jni/android_view_InputChannel.cpp
@@ -55,41 +55,25 @@ public:
inline std::shared_ptr<InputChannel> getInputChannel() { return mInputChannel; }
- void setDisposeCallback(InputChannelObjDisposeCallback callback, void* data);
void dispose(JNIEnv* env, jobject obj);
private:
std::shared_ptr<InputChannel> mInputChannel;
- InputChannelObjDisposeCallback mDisposeCallback;
- void* mDisposeData;
};
// ----------------------------------------------------------------------------
NativeInputChannel::NativeInputChannel(std::unique_ptr<InputChannel> inputChannel)
- : mInputChannel(std::move(inputChannel)), mDisposeCallback(nullptr) {}
+ : mInputChannel(std::move(inputChannel)) {}
NativeInputChannel::~NativeInputChannel() {
}
-void NativeInputChannel::setDisposeCallback(InputChannelObjDisposeCallback callback, void* data) {
- if (input_flags::remove_input_channel_from_windowstate()) {
- return;
- }
- mDisposeCallback = callback;
- mDisposeData = data;
-}
-
void NativeInputChannel::dispose(JNIEnv* env, jobject obj) {
if (!mInputChannel) {
return;
}
- if (mDisposeCallback) {
- mDisposeCallback(env, obj, mInputChannel, mDisposeData);
- mDisposeCallback = nullptr;
- mDisposeData = nullptr;
- }
mInputChannel.reset();
}
@@ -108,17 +92,6 @@ std::shared_ptr<InputChannel> android_view_InputChannel_getInputChannel(JNIEnv*
return nativeInputChannel != nullptr ? nativeInputChannel->getInputChannel() : nullptr;
}
-void android_view_InputChannel_setDisposeCallback(JNIEnv* env, jobject inputChannelObj,
- InputChannelObjDisposeCallback callback, void* data) {
- NativeInputChannel* nativeInputChannel =
- android_view_InputChannel_getNativeInputChannel(env, inputChannelObj);
- if (!nativeInputChannel || !nativeInputChannel->getInputChannel()) {
- ALOGW("Cannot set dispose callback because input channel object has not been initialized.");
- } else {
- nativeInputChannel->setDisposeCallback(callback, data);
- }
-}
-
static jlong android_view_InputChannel_createInputChannel(
JNIEnv* env, std::unique_ptr<InputChannel> inputChannel) {
std::unique_ptr<NativeInputChannel> nativeInputChannel =
diff --git a/libs/WindowManager/Shell/aconfig/OWNERS b/libs/WindowManager/Shell/aconfig/OWNERS
index 9eba0f2dea7b..eacadeacab36 100644
--- a/libs/WindowManager/Shell/aconfig/OWNERS
+++ b/libs/WindowManager/Shell/aconfig/OWNERS
@@ -1,3 +1,4 @@
# Owners for flag changes
madym@google.com
-hwwang@google.com \ No newline at end of file
+hwwang@google.com
+sqsun@google.com \ No newline at end of file
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index ab2804626361..592c7cdd070c 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -203,4 +203,11 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-} \ No newline at end of file
+}
+
+flag {
+ name: "enable_magnetic_split_divider"
+ namespace: "multitasking"
+ description: "Makes the split divider snap 'magnetically' to available snap points during drag"
+ bug: "383631946"
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt
index 2d6df43f67e0..3597ce0041d4 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubject.kt
@@ -22,14 +22,14 @@ import com.google.common.truth.Subject
import com.google.common.truth.Truth
/** Subclass of [Subject] to simplify verifying [FakeUiEvent] data */
-class UiEventSubject(metadata: FailureMetadata, private val actual: FakeUiEvent) :
+class UiEventSubject(metadata: FailureMetadata, private val actual: FakeUiEvent?) :
Subject(metadata, actual) {
/** Check that [FakeUiEvent] contains the expected data from the [bubble] passed id */
fun hasBubbleInfo(bubble: Bubble) {
- check("uid").that(actual.uid).isEqualTo(bubble.appUid)
- check("packageName").that(actual.packageName).isEqualTo(bubble.packageName)
- check("instanceId").that(actual.instanceId).isEqualTo(bubble.instanceId)
+ check("uid").that(actual?.uid).isEqualTo(bubble.appUid)
+ check("packageName").that(actual?.packageName).isEqualTo(bubble.packageName)
+ check("instanceId").that(actual?.instanceId).isEqualTo(bubble.instanceId)
}
companion object {
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml
index b35dc022e210..56e5dc717a7b 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_ic_minimize.xml
@@ -18,9 +18,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
- android:viewportHeight="24"
- android:viewportWidth="24">
+ android:viewportWidth="960"
+ android:viewportHeight="960">
<path
android:fillColor="#FF000000"
- android:pathData="M6,21V19H18V21Z"/>
+ android:pathData="M160,800L160,720L800,720L800,800L160,800Z"/>
</vector>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index 16e098b39004..e11babe5cb0e 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -39,8 +39,8 @@
<ImageView
android:id="@+id/application_icon"
- android:layout_width="@dimen/desktop_mode_caption_icon_radius"
- android:layout_height="@dimen/desktop_mode_caption_icon_radius"
+ android:layout_width="@dimen/desktop_mode_handle_menu_icon_radius"
+ android:layout_height="@dimen/desktop_mode_handle_menu_icon_radius"
android:layout_marginStart="10dp"
android:layout_marginEnd="12dp"
android:contentDescription="@string/app_icon_text"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index b8f0e3d40f08..9ebbf71138b0 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -570,7 +570,10 @@
<dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen>
<!-- The radius of the caption menu icon. -->
- <dimen name="desktop_mode_caption_icon_radius">32dp</dimen>
+ <dimen name="desktop_mode_caption_icon_radius">24dp</dimen>
+
+ <!-- The radius of the icon in the header menu's app info pill. -->
+ <dimen name="desktop_mode_handle_menu_icon_radius">32dp</dimen>
<!-- The radius of the caption menu shadow. -->
<dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
index 9ea0532f9450..b87c2054bea6 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
@@ -19,13 +19,10 @@ package com.android.wm.shell.shared.desktopmode
import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.app.TaskInfo
import android.content.Context
-import android.content.pm.ActivityInfo
-import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED
-import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION
-import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS
import android.content.pm.PackageManager
import android.window.DesktopModeFlags
import com.android.internal.R
+import com.android.internal.policy.DesktopModeCompatUtils
/**
* Class to decide whether to apply app compat policies in desktop mode.
@@ -60,22 +57,11 @@ class DesktopModeCompatPolicy(private val context: Context) {
hasFullscreenTransparentPermission(packageName))) &&
!isTopActivityNoDisplay)
- /**
- * Whether the caption insets should be excluded from configuration for system to handle.
- *
- * The treatment is enabled when all the of the following is true:
- * * Any flags to forcibly consume caption insets are enabled.
- * * Top activity have configuration coupled with insets.
- * * Task is not resizeable or [ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS]
- * is enabled.
- */
+ /** @see DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds */
fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean =
- DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue
- && isAnyForceConsumptionFlagsEnabled()
- && taskInfo.topActivityInfo?.let {
- isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled(
- OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS
- ))
+ taskInfo.topActivityInfo?.let {
+ DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds(it, taskInfo.isResizeable,
+ taskInfo.appCompatTaskInfo.hasOptOutEdgeToEdge())
} ?: false
/**
@@ -118,12 +104,4 @@ class DesktopModeCompatPolicy(private val context: Context) {
*/
private fun isPartOfDefaultHomePackageOrNoHomeAvailable(packageName: String?) =
defaultHomePackage == null || (packageName != null && packageName == defaultHomePackage)
-
- private fun isAnyForceConsumptionFlagsEnabled(): Boolean =
- DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue
- || DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue
-
- private fun isInsetsCoupledWithConfiguration(info: ActivityInfo): Boolean =
- !(info.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION)
- || info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED))
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index bc2ed3f35b45..179f03b8a975 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -98,6 +98,7 @@ import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopModeMoveToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger;
+import com.android.wm.shell.desktopmode.DesktopPipTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
@@ -780,6 +781,7 @@ public abstract class WMShellModule {
OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
DesksOrganizer desksOrganizer,
Optional<DesksTransitionObserver> desksTransitionObserver,
+ Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver,
UserProfileContexts userProfileContexts,
DesktopModeCompatPolicy desktopModeCompatPolicy,
DragToDisplayTransitionHandler dragToDisplayTransitionHandler,
@@ -823,6 +825,7 @@ public abstract class WMShellModule {
overviewToDesktopTransitionObserver,
desksOrganizer,
desksTransitionObserver.get(),
+ desktopPipTransitionObserver.get(),
userProfileContexts,
desktopModeCompatPolicy,
dragToDisplayTransitionHandler,
@@ -1225,6 +1228,7 @@ public abstract class WMShellModule {
Transitions transitions,
ShellTaskOrganizer shellTaskOrganizer,
Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
+ Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver,
Optional<BackAnimationController> backAnimationController,
DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
ShellInit shellInit) {
@@ -1237,6 +1241,7 @@ public abstract class WMShellModule {
transitions,
shellTaskOrganizer,
desktopMixedTransitionHandler.get(),
+ desktopPipTransitionObserver.get(),
backAnimationController.get(),
desktopWallpaperActivityTokenProvider,
shellInit)));
@@ -1258,6 +1263,19 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
+ static Optional<DesktopPipTransitionObserver> provideDesktopPipTransitionObserver(
+ Context context
+ ) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)
+ && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()) {
+ return Optional.of(
+ new DesktopPipTransitionObserver());
+ }
+ return Optional.empty();
+ }
+
+ @WMSingleton
+ @Provides
static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler(
Context context,
Transitions transitions,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt
new file mode 100644
index 000000000000..efd3866e1bc4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserver.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
+import android.os.IBinder
+import android.window.DesktopModeFlags
+import android.window.TransitionInfo
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+
+/**
+ * Observer of PiP in Desktop Mode transitions. At the moment, this is specifically tracking a PiP
+ * transition for a task that is entering PiP via the minimize button on the caption bar.
+ */
+class DesktopPipTransitionObserver {
+ private val pendingPipTransitions = mutableMapOf<IBinder, PendingPipTransition>()
+
+ /** Adds a pending PiP transition to be tracked. */
+ fun addPendingPipTransition(transition: PendingPipTransition) {
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return
+ pendingPipTransitions[transition.token] = transition
+ }
+
+ /**
+ * Called when any transition is ready, which may include transitions not tracked by this
+ * observer.
+ */
+ fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return
+ val pipTransition = pendingPipTransitions.remove(transition) ?: return
+
+ logD("Desktop PiP transition ready: %s", transition)
+ for (change in info.changes) {
+ val taskInfo = change.taskInfo
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue
+ }
+
+ if (
+ taskInfo.taskId == pipTransition.taskId &&
+ taskInfo.windowingMode == WINDOWING_MODE_PINNED
+ ) {
+ logD("Desktop PiP transition was successful")
+ pipTransition.onSuccess()
+ return
+ }
+ }
+ logD("Change with PiP task not found in Desktop PiP transition; likely failed")
+ }
+
+ /**
+ * Data tracked for a pending PiP transition.
+ *
+ * @property token the PiP transition that is started.
+ * @property taskId task id of the task entering PiP.
+ * @property onSuccess callback to be invoked if the PiP transition is successful.
+ */
+ data class PendingPipTransition(val token: IBinder, val taskId: Int, val onSuccess: () -> Unit)
+
+ private fun logD(msg: String, vararg arguments: Any?) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ private companion object {
+ private const val TAG = "DesktopPipTransitionObserver"
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 8636bc1f56c2..0c2ee4648a43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -68,7 +68,6 @@ class DesktopRepository(
* @property topTransparentFullscreenTaskId the task id of any current top transparent
* fullscreen task launched on top of the desk. Cleared when the transparent task is closed or
* sent to back. (top is at index 0).
- * @property pipTaskId the task id of PiP task entered while in Desktop Mode.
*/
private data class Desk(
val deskId: Int,
@@ -81,7 +80,6 @@ class DesktopRepository(
val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
var fullImmersiveTaskId: Int? = null,
var topTransparentFullscreenTaskId: Int? = null,
- var pipTaskId: Int? = null,
) {
fun deepCopy(): Desk =
Desk(
@@ -94,7 +92,6 @@ class DesktopRepository(
freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
fullImmersiveTaskId = fullImmersiveTaskId,
topTransparentFullscreenTaskId = topTransparentFullscreenTaskId,
- pipTaskId = pipTaskId,
)
// TODO: b/362720497 - remove when multi-desktops is enabled where instances aren't
@@ -107,7 +104,6 @@ class DesktopRepository(
freeformTasksInZOrder.clear()
fullImmersiveTaskId = null
topTransparentFullscreenTaskId = null
- pipTaskId = null
}
}
@@ -127,9 +123,6 @@ class DesktopRepository(
/* Tracks last bounds of task before toggled to immersive state. */
private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()
- /* Callback for when a pending PiP transition has been aborted. */
- private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null
-
private var desktopGestureExclusionListener: Consumer<Region>? = null
private var desktopGestureExclusionExecutor: Executor? = null
@@ -611,57 +604,6 @@ class DesktopRepository(
}
/**
- * Set whether the given task is the Desktop-entered PiP task in this display's active desk.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
- */
- fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) {
- val activeDesk =
- desktopData.getActiveDesk(displayId)
- ?: error("Expected active desk in display: $displayId")
- if (enterPip) {
- activeDesk.pipTaskId = taskId
- } else {
- activeDesk.pipTaskId =
- if (activeDesk.pipTaskId == taskId) null
- else {
- logW(
- "setTaskInPip: taskId=%d did not match saved taskId=%d",
- taskId,
- activeDesk.pipTaskId,
- )
- activeDesk.pipTaskId
- }
- }
- }
-
- /**
- * Returns whether the given task is the Desktop-entered PiP task in this display's active desk.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
- */
- fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean =
- desktopData.getActiveDesk(displayId)?.pipTaskId == taskId
-
- /**
- * Saves callback to handle a pending PiP transition being aborted.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
- */
- fun setOnPipAbortedCallback(callbackIfPipAborted: ((displayId: Int, pipTaskId: Int) -> Unit)?) {
- onPipAbortedCallback = callbackIfPipAborted
- }
-
- /**
- * Invokes callback to handle a pending PiP transition with the given task id being aborted.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
- */
- fun onPipAborted(displayId: Int, pipTaskId: Int) {
- onPipAbortedCallback?.invoke(displayId, pipTaskId)
- }
-
- /**
* Set whether the given task is the full-immersive task in this display's active desk.
*
* TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with
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 c28fdcb44f5b..1f0774c24143 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
@@ -215,6 +215,7 @@ class DesktopTasksController(
private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
private val desksOrganizer: DesksOrganizer,
private val desksTransitionObserver: DesksTransitionObserver,
+ private val desktopPipTransitionObserver: DesktopPipTransitionObserver,
private val userProfileContexts: UserProfileContexts,
private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
@@ -788,10 +789,30 @@ class DesktopTasksController(
fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
val wct = WindowContainerTransaction()
-
+ val taskId = taskInfo.taskId
+ val displayId = taskInfo.displayId
+ val deskId =
+ taskRepository.getDeskIdForTask(taskInfo.taskId)
+ ?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ logW("minimizeTask: desk not found for task: ${taskInfo.taskId}")
+ return
+ } else {
+ getDefaultDeskId(taskInfo.displayId)
+ }
+ val isLastTask =
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ taskRepository.isOnlyVisibleNonClosingTaskInDesk(
+ taskId = taskId,
+ deskId = checkNotNull(deskId) { "Expected non-null deskId" },
+ displayId = displayId,
+ )
+ } else {
+ taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
+ }
val isMinimizingToPip =
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
- (taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false)
+ (taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false)
+
// If task is going to PiP, start a PiP transition instead of a minimize transition
if (isMinimizingToPip) {
val requestInfo =
@@ -805,75 +826,60 @@ class DesktopTasksController(
)
val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
wct.merge(requestRes.second, true)
- freeformTaskTransitionStarter.startPipTransition(wct)
- taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true)
- taskRepository.setOnPipAbortedCallback { displayId, taskId ->
- minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!, minimizeReason)
- taskRepository.setTaskInPip(displayId, taskId, enterPip = false)
- }
- return
- }
-
- minimizeTaskInner(taskInfo, minimizeReason)
- }
- private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
- val taskId = taskInfo.taskId
- val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
- if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- logW("minimizeTaskInner: desk not found for task: ${taskInfo.taskId}")
- return
- }
- val displayId = taskInfo.displayId
- val wct = WindowContainerTransaction()
-
- snapEventHandler.removeTaskIfTiled(displayId, taskId)
- val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
- val desktopExitRunnable =
- performDesktopExitCleanUp(
- wct = wct,
- deskId = deskId,
- displayId = displayId,
- willExitDesktop = willExitDesktop,
- )
- // Notify immersive handler as it might need to exit immersive state.
- val exitResult =
- desktopImmersiveController.exitImmersiveIfApplicable(
- wct = wct,
- taskInfo = taskInfo,
- reason = DesktopImmersiveController.ExitReason.MINIMIZED,
- )
- if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- desksOrganizer.minimizeTask(
- wct = wct,
- deskId = checkNotNull(deskId) { "Expected non-null deskId" },
- task = taskInfo,
+ desktopPipTransitionObserver.addPendingPipTransition(
+ DesktopPipTransitionObserver.PendingPipTransition(
+ token = freeformTaskTransitionStarter.startPipTransition(wct),
+ taskId = taskInfo.taskId,
+ onSuccess = {
+ onDesktopTaskEnteredPip(
+ taskId = taskId,
+ deskId = deskId,
+ displayId = taskInfo.displayId,
+ taskIsLastVisibleTaskBeforePip = isLastTask,
+ )
+ },
+ )
)
} else {
- wct.reorder(taskInfo.token, /* onTop= */ false)
- }
- val isLastTask =
+ snapEventHandler.removeTaskIfTiled(displayId, taskId)
+ val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
+ val desktopExitRunnable =
+ performDesktopExitCleanUp(
+ wct = wct,
+ deskId = deskId,
+ displayId = displayId,
+ willExitDesktop = willExitDesktop,
+ )
+ // Notify immersive handler as it might need to exit immersive state.
+ val exitResult =
+ desktopImmersiveController.exitImmersiveIfApplicable(
+ wct = wct,
+ taskInfo = taskInfo,
+ reason = DesktopImmersiveController.ExitReason.MINIMIZED,
+ )
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- taskRepository.isOnlyVisibleNonClosingTaskInDesk(
- taskId = taskId,
+ desksOrganizer.minimizeTask(
+ wct = wct,
deskId = checkNotNull(deskId) { "Expected non-null deskId" },
- displayId = displayId,
+ task = taskInfo,
)
} else {
- taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
+ wct.reorder(taskInfo.token, /* onTop= */ false)
}
- val transition =
- freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
- desktopTasksLimiter.ifPresent {
- it.addPendingMinimizeChange(
- transition = transition,
- displayId = displayId,
- taskId = taskId,
- minimizeReason = minimizeReason,
- )
+ val transition =
+ freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
+ desktopTasksLimiter.ifPresent {
+ it.addPendingMinimizeChange(
+ transition = transition,
+ displayId = displayId,
+ taskId = taskId,
+ minimizeReason = minimizeReason,
+ )
+ }
+ exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
+ desktopExitRunnable?.invoke(transition)
}
- exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
- desktopExitRunnable?.invoke(transition)
}
/** Move a task with given `taskId` to fullscreen */
@@ -1841,7 +1847,11 @@ class DesktopTasksController(
displayId: Int,
forceExitDesktop: Boolean,
): Boolean {
- if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ if (
+ forceExitDesktop &&
+ (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue)
+ ) {
// |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
// explicitly going fullscreen, so there's no point in checking the desktop state.
return true
@@ -1858,6 +1868,33 @@ class DesktopTasksController(
return true
}
+ /** Potentially perform Desktop cleanup after a task successfully enters PiP. */
+ @VisibleForTesting
+ fun onDesktopTaskEnteredPip(
+ taskId: Int,
+ deskId: Int,
+ displayId: Int,
+ taskIsLastVisibleTaskBeforePip: Boolean,
+ ) {
+ if (
+ !willExitDesktop(taskId, displayId, forceExitDesktop = taskIsLastVisibleTaskBeforePip)
+ ) {
+ return
+ }
+
+ val wct = WindowContainerTransaction()
+ val desktopExitRunnable =
+ performDesktopExitCleanUp(
+ wct = wct,
+ deskId = deskId,
+ displayId = displayId,
+ willExitDesktop = true,
+ )
+
+ val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
+ desktopExitRunnable?.invoke(transition)
+ }
+
private fun performDesktopExitCleanupIfNeeded(
taskId: Int,
deskId: Int? = null,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 7dabeb7c9d15..c670ac3c4488 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -23,7 +23,6 @@ import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
@@ -41,8 +40,6 @@ import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
-import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
-import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
/**
* A [Transitions.TransitionObserver] that observes shell transitions and updates the
@@ -55,6 +52,7 @@ class DesktopTasksTransitionObserver(
private val transitions: Transitions,
private val shellTaskOrganizer: ShellTaskOrganizer,
private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
+ private val desktopPipTransitionObserver: DesktopPipTransitionObserver,
private val backAnimationController: BackAnimationController,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
shellInit: ShellInit,
@@ -63,8 +61,6 @@ class DesktopTasksTransitionObserver(
data class CloseWallpaperTransition(val transition: IBinder, val displayId: Int)
private var transitionToCloseWallpaper: CloseWallpaperTransition? = null
- /* Pending PiP transition and its associated display id and task id. */
- private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null
private var currentProfileId: Int
init {
@@ -98,33 +94,7 @@ class DesktopTasksTransitionObserver(
removeTaskIfNeeded(info)
}
removeWallpaperOnLastTaskClosingIfNeeded(transition, info)
-
- val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
- info.changes.forEach { change ->
- change.taskInfo?.let { taskInfo ->
- if (
- DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
- desktopRepository.isTaskMinimizedPipInDisplay(
- taskInfo.displayId,
- taskInfo.taskId,
- )
- ) {
- when (info.type) {
- TRANSIT_PIP ->
- pendingPipTransitionAndPipTask =
- Triple(transition, taskInfo.displayId, taskInfo.taskId)
-
- TRANSIT_EXIT_PIP,
- TRANSIT_REMOVE_PIP ->
- desktopRepository.setTaskInPip(
- taskInfo.displayId,
- taskInfo.taskId,
- enterPip = false,
- )
- }
- }
- }
- }
+ desktopPipTransitionObserver.onTransitionReady(transition, info)
}
private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -299,18 +269,6 @@ class DesktopTasksTransitionObserver(
}
}
transitionToCloseWallpaper = null
- } else if (pendingPipTransitionAndPipTask?.first == transition) {
- val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
- if (aborted) {
- pendingPipTransitionAndPipTask?.let {
- desktopRepository.onPipAborted(
- /*displayId=*/ it.second,
- /* taskId=*/ it.third,
- )
- }
- }
- desktopRepository.setOnPipAbortedCallback(null)
- pendingPipTransitionAndPipTask = null
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 275d7b73a112..2aebcdcc3bf5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -61,10 +61,10 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var displayController: DisplayController
@Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
- @Mock private lateinit var mockDesktopRepositoryInitializer: DesktopRepositoryInitializer
@Mock private lateinit var mockDesktopRepository: DesktopRepository
@Mock private lateinit var mockDesktopTasksController: DesktopTasksController
@Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController
+ private val desktopRepositoryInitializer = FakeDesktopRepositoryInitializer()
private val testScope = TestScope()
private lateinit var mockitoSession: StaticMockitoSession
@@ -90,7 +90,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
shellInit,
testScope.backgroundScope,
displayController,
- mockDesktopRepositoryInitializer,
+ desktopRepositoryInitializer,
mockDesktopUserRepositories,
mockDesktopTasksController,
desktopDisplayModeController,
@@ -109,12 +109,10 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Test
fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() =
testScope.runTest {
- val stateFlow = MutableStateFlow(false)
- whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
- stateFlow.emit(true)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
runCurrent()
verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
@@ -123,8 +121,6 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Test
fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() =
testScope.runTest {
- val stateFlow = MutableStateFlow(false)
- whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
@@ -136,13 +132,11 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Test
fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() =
testScope.runTest {
- val stateFlow = MutableStateFlow(false)
- whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
- stateFlow.emit(true)
- stateFlow.emit(true)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
runCurrent()
verify(mockDesktopTasksController, times(1)).createDesk(DEFAULT_DISPLAY)
@@ -151,13 +145,11 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Test
fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() =
testScope.runTest {
- val stateFlow = MutableStateFlow(false)
- whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
- stateFlow.emit(true)
+ desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
runCurrent()
verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
@@ -203,4 +195,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId)
verify(desktopDisplayModeController).refreshDisplayWindowingMode()
}
+
+ private class FakeDesktopRepositoryInitializer : DesktopRepositoryInitializer {
+ override var deskRecreationFactory: DesktopRepositoryInitializer.DeskRecreationFactory =
+ DesktopRepositoryInitializer.DeskRecreationFactory { _, _, deskId -> deskId }
+
+ override val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ override fun initialize(userRepositories: DesktopUserRepositories) {
+ isInitialized.value = true
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt
new file mode 100644
index 000000000000..ef394d81cc57
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopPipTransitionObserverTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
+import android.os.Binder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.WindowManager.TRANSIT_PIP
+import android.window.TransitionInfo
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Tests for [DesktopPipTransitionObserver].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:DesktopPipTransitionObserverTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopPipTransitionObserverTest : ShellTestCase() {
+
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+ private lateinit var observer: DesktopPipTransitionObserver
+
+ private val transition = Binder()
+ private var onSuccessInvokedCount = 0
+
+ @Before
+ fun setUp() {
+ observer = DesktopPipTransitionObserver()
+
+ onSuccessInvokedCount = 0
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun onTransitionReady_taskInPinnedWindowingMode_onSuccessInvoked() {
+ val taskId = 1
+ val pipTransition = createPendingPipTransition(taskId)
+ val successfulChange = createChange(taskId, WINDOWING_MODE_PINNED)
+ observer.addPendingPipTransition(pipTransition)
+
+ observer.onTransitionReady(
+ transition = transition,
+ info = TransitionInfo(
+ TRANSIT_PIP, /* flags= */
+ 0
+ ).apply { addChange(successfulChange) },
+ )
+
+ assertThat(onSuccessInvokedCount).isEqualTo(1)
+ }
+
+ private fun createPendingPipTransition(
+ taskId: Int
+ ): DesktopPipTransitionObserver.PendingPipTransition {
+ return DesktopPipTransitionObserver.PendingPipTransition(
+ token = transition,
+ taskId = taskId,
+ onSuccess = { onSuccessInvokedCount += 1 },
+ )
+ }
+
+ private fun createChange(taskId: Int, windowingMode: Int): TransitionInfo.Change {
+ return TransitionInfo.Change(mock(), mock()).apply {
+ taskInfo =
+ TestRunningTaskInfoBuilder()
+ .setTaskId(taskId)
+ .setWindowingMode(windowingMode)
+ .build()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index f84a1a38bdfc..3813f752cae8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -1225,36 +1225,6 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
}
@Test
- fun setTaskInPip_savedAsMinimizedPipInDisplay() {
- assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
-
- repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
-
- assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
- }
-
- @Test
- fun removeTaskInPip_removedAsMinimizedPipInDisplay() {
- repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
- assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
-
- repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false)
-
- assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse()
- }
-
- @Test
- fun setTaskInPip_multipleDisplays_bothAreInPip() {
- repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
- repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
- repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true)
- repo.setTaskInPip(SECOND_DISPLAY, taskId = 2, enterPip = true)
-
- assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue()
- assertThat(repo.isTaskMinimizedPipInDisplay(SECOND_DISPLAY, taskId = 2)).isTrue()
- }
-
- @Test
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun addTask_deskDoesNotExists_createsDesk() {
repo.addTask(displayId = 999, taskId = 6, isVisible = true)
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 b2553b0bd30f..14af57372ed6 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
@@ -264,6 +264,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Mock private lateinit var desksOrganizer: DesksOrganizer
@Mock private lateinit var userProfileContexts: UserProfileContexts
@Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
+ @Mock private lateinit var desktopPipTransitionObserver: DesktopPipTransitionObserver
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var mockDisplayContext: Context
@Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler
@@ -392,6 +393,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
whenever(userProfileContexts[anyInt()]).thenReturn(context)
whenever(userProfileContexts.getOrCreate(anyInt())).thenReturn(context)
+ whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder())
controller = createController()
controller.setSplitScreenController(splitScreenController)
@@ -456,6 +458,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
overviewToDesktopTransitionObserver,
desksOrganizer,
desksTransitionsObserver,
+ desktopPipTransitionObserver,
userProfileContexts,
desktopModeCompatPolicy,
dragToDisplayTransitionHandler,
@@ -3470,6 +3473,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() {
val task = setUpPipTask(autoEnterEnabled = true)
val handler = mock(TransitionHandler::class.java)
@@ -3484,6 +3488,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() {
val task = setUpPipTask(autoEnterEnabled = false)
whenever(
@@ -3503,6 +3508,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
+ fun onDesktopTaskEnteredPip_pipIsLastTask_removesWallpaper() {
+ val task = setUpPipTask(autoEnterEnabled = true)
+
+ controller.onDesktopTaskEnteredPip(
+ taskId = task.taskId,
+ deskId = DEFAULT_DISPLAY,
+ displayId = task.displayId,
+ taskIsLastVisibleTaskBeforePip = true,
+ )
+
+ // Wallpaper is moved to the back
+ val wct = getLatestTransition()
+ wct.assertReorder(wallpaperToken, /* toTop= */ false)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun onDesktopTaskEnteredPip_pipIsLastTask_deactivatesDesk() {
+ val deskId = DEFAULT_DISPLAY
+ val task = setUpPipTask(autoEnterEnabled = true, deskId = deskId)
+ val transition = Binder()
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.onDesktopTaskEnteredPip(
+ taskId = task.taskId,
+ deskId = deskId,
+ displayId = task.displayId,
+ taskIsLastVisibleTaskBeforePip = true,
+ )
+
+ verify(desksOrganizer).deactivateDesk(any(), eq(deskId))
+ verify(desksTransitionsObserver)
+ .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun onDesktopTaskEnteredPip_pipIsLastTask_launchesHome() {
+ val task = setUpPipTask(autoEnterEnabled = true)
+
+ controller.onDesktopTaskEnteredPip(
+ taskId = task.taskId,
+ deskId = DEFAULT_DISPLAY,
+ displayId = task.displayId,
+ taskIsLastVisibleTaskBeforePip = true,
+ )
+
+ val wct = getLatestTransition()
+ wct.assertPendingIntent(launchHomeIntent(DEFAULT_DISPLAY))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+ fun onDesktopTaskEnteredPip_pipIsNotLastTask_doesntExitDesktopMode() {
+ val task = setUpPipTask(autoEnterEnabled = true)
+ val deskId = DEFAULT_DISPLAY
+ setUpFreeformTask(deskId = deskId) // launch another freeform task
+ val transition = Binder()
+ whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
+
+ controller.onDesktopTaskEnteredPip(
+ taskId = task.taskId,
+ deskId = deskId,
+ displayId = task.displayId,
+ taskIsLastVisibleTaskBeforePip = false,
+ )
+
+ // No transition to exit Desktop mode is started
+ verifyWCTNotExecuted()
+ verify(desktopModeEnterExitTransitionListener, never())
+ .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
+ verify(desksOrganizer, never()).deactivateDesk(any(), eq(deskId))
+ verify(desksTransitionsObserver, never())
+ .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId))
+ }
+
+ @Test
fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() {
val task = setUpFreeformTask(active = true)
val transition = Binder()
@@ -7615,8 +7704,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
return task
}
- private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo =
- setUpFreeformTask().apply {
+ private fun setUpPipTask(
+ autoEnterEnabled: Boolean,
+ displayId: Int = DEFAULT_DISPLAY,
+ deskId: Int = DEFAULT_DISPLAY,
+ ): RunningTaskInfo =
+ setUpFreeformTask(displayId = displayId, deskId = deskId).apply {
pictureInPictureParams =
PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build()
}
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 a7dc706eb6c9..ec64c2fa2337 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -22,7 +22,6 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.os.Binder
import android.os.IBinder
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
@@ -30,7 +29,6 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IWindowContainerToken
@@ -41,7 +39,6 @@ import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
import com.android.modules.utils.testing.ExtendedMockitoRule
import com.android.window.flags.Flags
-import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP
import com.android.wm.shell.MockToken
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.back.BackAnimationController
@@ -51,8 +48,6 @@ import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpape
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
-import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
-import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
@@ -90,6 +85,7 @@ class DesktopTasksTransitionObserverTest {
private val userRepositories = mock<DesktopUserRepositories>()
private val taskRepository = mock<DesktopRepository>()
private val mixedHandler = mock<DesktopMixedTransitionHandler>()
+ private val pipTransitionObserver = mock<DesktopPipTransitionObserver>()
private val backAnimationController = mock<BackAnimationController>()
private val desktopWallpaperActivityTokenProvider =
mock<DesktopWallpaperActivityTokenProvider>()
@@ -114,6 +110,7 @@ class DesktopTasksTransitionObserverTest {
transitions,
shellTaskOrganizer,
mixedHandler,
+ pipTransitionObserver,
backAnimationController,
desktopWallpaperActivityTokenProvider,
shellInit,
@@ -349,56 +346,6 @@ class DesktopTasksTransitionObserverTest {
verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId)
}
- @Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
- fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() {
- val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
- val pipTransition = Binder()
- whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
-
- transitionObserver.onTransitionReady(
- transition = pipTransition,
- info = createOpenChangeTransition(task, type = TRANSIT_PIP),
- startTransaction = mock(),
- finishTransaction = mock(),
- )
- transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true)
-
- verify(taskRepository).onPipAborted(task.displayId, task.taskId)
- }
-
- @Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
- fun exitPipTransition_taskRepositoryClearTaskInPip() {
- val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
- whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
-
- transitionObserver.onTransitionReady(
- transition = mock(),
- info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP),
- startTransaction = mock(),
- finishTransaction = mock(),
- )
-
- verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
- }
-
- @Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
- fun removePipTransition_taskRepositoryClearTaskInPip() {
- val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
- whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true)
-
- transitionObserver.onTransitionReady(
- transition = mock(),
- info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP),
- startTransaction = mock(),
- finishTransaction = mock(),
- )
-
- verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false)
- }
-
private fun createBackNavigationTransition(
task: RunningTaskInfo?,
type: Int = TRANSIT_TO_BACK,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index e246329446dc..5dff21860ef4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -211,11 +211,19 @@ public class StageCoordinatorTests extends ShellTestCase {
when(mSplitLayout.getDividerLeash()).thenReturn(dividerLeash);
mRootTask = new TestRunningTaskInfoBuilder().build();
- SurfaceControl rootLeash = new SurfaceControl.Builder().setName("test").build();
+ SurfaceControl rootLeash = new SurfaceControl.Builder().setName("splitRoot").build();
mStageCoordinator.onTaskAppeared(mRootTask, rootLeash);
mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
+ SurfaceControl mainRootLeash = new SurfaceControl.Builder().setName("mainRoot").build();
+ SurfaceControl sideRootLeash = new SurfaceControl.Builder().setName("sideRoot").build();
+ mMainStage.mRootLeash = mainRootLeash;
+ mSideStage.mRootLeash = sideRootLeash;
+ SurfaceControl mainDimLayer = new SurfaceControl.Builder().setName("mainDim").build();
+ SurfaceControl sideDimLayer = new SurfaceControl.Builder().setName("sideDim").build();
+ mMainStage.mDimLayer = mainDimLayer;
+ mSideStage.mDimLayer = sideDimLayer;
doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
diff --git a/location/java/android/location/GnssClock.java b/location/java/android/location/GnssClock.java
index 62f50b57520c..6930f365adc1 100644
--- a/location/java/android/location/GnssClock.java
+++ b/location/java/android/location/GnssClock.java
@@ -349,7 +349,7 @@ public final class GnssClock implements Parcelable {
* Gets the clock's Drift in nanoseconds per second.
*
* <p>This value is the instantaneous time-derivative of the value provided by
- * {@link #getBiasNanos()}.
+ * the sum of {@link #getFullBiasNanos()} and {@link #getBiasNanos()}.
*
* <p>A positive value indicates that the frequency is higher than the nominal (e.g. GPS master
* clock) frequency. The error estimate for this reported drift is
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index e39a0aa8717e..48e2f4e15238 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -242,3 +242,13 @@ flag {
description: "Fallbacks to the default handling for volume adjustment when media session has fixed volume handling and its app is in the foreground and setting a media controller."
bug: "293743975"
}
+
+flag {
+ name: "fix_output_media_item_list_index_out_of_bounds_exception"
+ namespace: "media_better_together"
+ description: "Fixes a bug of causing IndexOutOfBoundsException when building media item list."
+ bug: "398246089"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/media/java/android/media/quality/Android.bp b/media/java/android/media/quality/Android.bp
index 080d5266ccb7..f620144e2880 100644
--- a/media/java/android/media/quality/Android.bp
+++ b/media/java/android/media/quality/Android.bp
@@ -15,6 +15,30 @@ filegroup {
path: "aidl",
}
+cc_library_headers {
+ name: "media_quality_headers",
+ export_include_dirs: ["include"],
+}
+
+cc_library_shared {
+ name: "libmedia_quality_include",
+
+ export_include_dirs: ["include"],
+ cflags: [
+ "-Wno-unused-variable",
+ "-Wunused-parameter",
+ ],
+
+ shared_libs: [
+ "libbinder",
+ "libutils",
+ ],
+
+ srcs: [
+ ":framework-media-quality-sources-aidl",
+ ],
+}
+
aidl_interface {
name: "media_quality_aidl_interface",
unstable: true,
@@ -24,7 +48,8 @@ aidl_interface {
enabled: true,
},
cpp: {
- enabled: false,
+ additional_shared_libraries: ["libmedia_quality_include"],
+ enabled: true,
},
ndk: {
enabled: false,
diff --git a/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl
index 2851306f6e4d..d2cf140632ab 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/ActiveProcessingPicture.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable ActiveProcessingPicture; \ No newline at end of file
+parcelable ActiveProcessingPicture cpp_header "quality/MediaQualityManager.h"; \ No newline at end of file
diff --git a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl
index 174cd461e846..d53860fdf9ad 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightEvent.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable AmbientBacklightEvent;
+parcelable AmbientBacklightEvent cpp_header "quality/MediaQualityManager.h";
diff --git a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl
index b95a474fbf90..a935b49b5d23 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightMetadata.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable AmbientBacklightMetadata; \ No newline at end of file
+parcelable AmbientBacklightMetadata cpp_header "quality/MediaQualityManager.h"; \ No newline at end of file
diff --git a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl
index e2cdd03194cd..051aef80b948 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/AmbientBacklightSettings.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable AmbientBacklightSettings;
+parcelable AmbientBacklightSettings cpp_header "quality/MediaQualityManager.h";
diff --git a/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl
index eb2ac97916f3..ea848576e026 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/ParameterCapability.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable ParameterCapability;
+parcelable ParameterCapability cpp_header "quality/MediaQualityManager.h";
diff --git a/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl
index 41d018b12f33..b0fe3f5538f4 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfile.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable PictureProfile;
+parcelable PictureProfile cpp_header "quality/MediaQualityManager.h";
diff --git a/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl
index 5d14631dbb73..0582938b6ea7 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/PictureProfileHandle.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable PictureProfileHandle;
+parcelable PictureProfileHandle cpp_header "quality/MediaQualityManager.h";
diff --git a/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl
index e79fcaac97be..d93231fbf7e0 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/SoundProfile.aidl
@@ -16,4 +16,4 @@
package android.media.quality;
-parcelable SoundProfile;
+parcelable SoundProfile cpp_header "quality/MediaQualityManager.h";
diff --git a/media/java/android/media/quality/include/quality/MediaQualityManager.h b/media/java/android/media/quality/include/quality/MediaQualityManager.h
new file mode 100644
index 000000000000..8c31667077c3
--- /dev/null
+++ b/media/java/android/media/quality/include/quality/MediaQualityManager.h
@@ -0,0 +1,127 @@
+#ifndef ANDROID_MEDIA_QUALITY_MANAGER_H
+#define ANDROID_MEDIA_QUALITY_MANAGER_H
+
+
+namespace android {
+namespace media {
+namespace quality {
+
+// TODO: implement writeToParcel and readFromParcel
+
+class PictureProfileHandle : public Parcelable {
+ public:
+ PictureProfileHandle() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class SoundProfile : public Parcelable {
+ public:
+ SoundProfile() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class PictureProfile : public Parcelable {
+ public:
+ PictureProfile() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class ActiveProcessingPicture : public Parcelable {
+ public:
+ ActiveProcessingPicture() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class AmbientBacklightEvent : public Parcelable {
+ public:
+ AmbientBacklightEvent() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class AmbientBacklightMetadata : public Parcelable {
+ public:
+ AmbientBacklightMetadata() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class AmbientBacklightSettings : public Parcelable {
+ public:
+ AmbientBacklightSettings() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+class ParameterCapability : public Parcelable {
+ public:
+ ParameterCapability() {}
+ status_t writeToParcel(android::Parcel*) const override {
+ return 0;
+ }
+ status_t readFromParcel(const android::Parcel*) override {
+ return 0;
+ }
+ std::string toString() const {
+ return "";
+ }
+};
+
+} // namespace quality
+} // namespace media
+} // namespace android
+
+#endif
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
index 8ad96a5bcb37..62b134279267 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -77,6 +77,16 @@ internal constructor(
list.apply { add(toIndex, removeAt(fromIndex)) }
}
+ /** Swap the two items in the list with the given indices. */
+ fun swapItems(index1: Int, index2: Int) {
+ list.apply {
+ val item1 = get(index1)
+ val item2 = get(index2)
+ set(index2, item1)
+ set(index1, item2)
+ }
+ }
+
/** Remove widget from the list and the database. */
fun onRemove(indexToRemove: Int) {
if (list[indexToRemove].isWidgetContent()) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
index 0aef7f2c7063..dda388aeeac6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -18,8 +18,10 @@ package com.android.systemui.communal.ui.compose
import android.content.ClipDescription
import android.view.DragEvent
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
@@ -37,6 +39,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.android.systemui.Flags.communalWidgetResizing
+import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.util.WidgetPickerIntentUtils
@@ -51,13 +54,14 @@ import kotlinx.coroutines.launch
* @see dragAndDropTarget
*/
@Composable
-internal fun rememberDragAndDropTargetState(
+fun rememberDragAndDropTargetState(
gridState: LazyGridState,
contentOffset: Offset,
contentListState: ContentListState,
): DragAndDropTargetState {
val scope = rememberCoroutineScope()
val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
+
val state =
remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) {
DragAndDropTargetState(
@@ -68,11 +72,9 @@ internal fun rememberDragAndDropTargetState(
scope = scope,
)
}
- LaunchedEffect(state) {
- for (diff in state.scrollChannel) {
- gridState.scrollBy(diff)
- }
- }
+
+ LaunchedEffect(state) { state.processScrollRequests(scope) }
+
return state
}
@@ -83,7 +85,7 @@ internal fun rememberDragAndDropTargetState(
* @see DragEvent
*/
@Composable
-internal fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetState): Modifier {
+fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetState): Modifier {
val state by rememberUpdatedState(dragDropTargetState)
return this then
@@ -132,13 +134,79 @@ internal fun Modifier.dragAndDropTarget(dragDropTargetState: DragAndDropTargetSt
* other activities. [GridDragDropState] on the other hand, handles dragging of existing items in
* the communal hub grid.
*/
-internal class DragAndDropTargetState(
+class DragAndDropTargetState(
+ state: LazyGridState,
+ contentOffset: Offset,
+ contentListState: ContentListState,
+ autoScrollThreshold: Float,
+ scope: CoroutineScope,
+) {
+ private val dragDropState: DragAndDropTargetStateInternal =
+ if (glanceableHubV2()) {
+ DragAndDropTargetStateV2(
+ state = state,
+ contentListState = contentListState,
+ scope = scope,
+ autoScrollThreshold = autoScrollThreshold,
+ contentOffset = contentOffset,
+ )
+ } else {
+ DragAndDropTargetStateV1(
+ state = state,
+ contentListState = contentListState,
+ scope = scope,
+ autoScrollThreshold = autoScrollThreshold,
+ contentOffset = contentOffset,
+ )
+ }
+
+ fun onStarted() = dragDropState.onStarted()
+
+ fun onMoved(event: DragAndDropEvent) = dragDropState.onMoved(event)
+
+ fun onDrop(event: DragAndDropEvent) = dragDropState.onDrop(event)
+
+ fun onEnded() = dragDropState.onEnded()
+
+ fun onExited() = dragDropState.onExited()
+
+ suspend fun processScrollRequests(coroutineScope: CoroutineScope) =
+ dragDropState.processScrollRequests(coroutineScope)
+}
+
+/**
+ * A private interface defining the API for handling drag-and-drop operations. There will be two
+ * implementations of this interface: V1 for devices that do not have the glanceable_hub_v2 flag
+ * enabled, and V2 for devices that do have that flag enabled.
+ *
+ * TODO(b/400789179): Remove this interface and the V1 implementation once glanceable_hub_v2 has
+ * shipped.
+ */
+private interface DragAndDropTargetStateInternal {
+ fun onStarted() = Unit
+
+ fun onMoved(event: DragAndDropEvent) = Unit
+
+ fun onDrop(event: DragAndDropEvent): Boolean = false
+
+ fun onEnded() = Unit
+
+ fun onExited() = Unit
+
+ suspend fun processScrollRequests(coroutineScope: CoroutineScope) = Unit
+}
+
+/**
+ * The V1 implementation of DragAndDropTargetStateInternal to be used when the glanceable_hub_v2
+ * flag is disabled.
+ */
+private class DragAndDropTargetStateV1(
private val state: LazyGridState,
private val contentOffset: Offset,
private val contentListState: ContentListState,
private val autoScrollThreshold: Float,
private val scope: CoroutineScope,
-) {
+) : DragAndDropTargetStateInternal {
/**
* The placeholder item that is treated as if it is being dragged across the grid. It is added
* to grid once drag and drop event is started and removed when event ends.
@@ -147,15 +215,21 @@ internal class DragAndDropTargetState(
private var placeHolderIndex: Int? = null
private var previousTargetItemKey: Any? = null
- internal val scrollChannel = Channel<Float>()
+ private val scrollChannel = Channel<Float>()
- fun onStarted() {
+ override suspend fun processScrollRequests(coroutineScope: CoroutineScope) {
+ for (diff in scrollChannel) {
+ state.scrollBy(diff)
+ }
+ }
+
+ override fun onStarted() {
// assume item will be added to the end.
contentListState.list.add(placeHolder)
placeHolderIndex = contentListState.list.size - 1
}
- fun onMoved(event: DragAndDropEvent) {
+ override fun onMoved(event: DragAndDropEvent) {
val dragOffset = event.toOffset()
val targetItem =
@@ -201,7 +275,7 @@ internal class DragAndDropTargetState(
}
}
- fun onDrop(event: DragAndDropEvent): Boolean {
+ override fun onDrop(event: DragAndDropEvent): Boolean {
return placeHolderIndex?.let { dropIndex ->
val widgetExtra = event.maybeWidgetExtra() ?: return false
val (componentName, user) = widgetExtra
@@ -219,13 +293,13 @@ internal class DragAndDropTargetState(
} ?: false
}
- fun onEnded() {
+ override fun onEnded() {
placeHolderIndex = null
previousTargetItemKey = null
contentListState.list.remove(placeHolder)
}
- fun onExited() {
+ override fun onExited() {
onEnded()
}
@@ -257,16 +331,186 @@ internal class DragAndDropTargetState(
contentListState.onMove(currentIndex, index)
}
}
+}
+/**
+ * The V2 implementation of DragAndDropTargetStateInternal to be used when the glanceable_hub_v2
+ * flag is enabled.
+ */
+private class DragAndDropTargetStateV2(
+ private val state: LazyGridState,
+ private val contentOffset: Offset,
+ private val contentListState: ContentListState,
+ private val autoScrollThreshold: Float,
+ private val scope: CoroutineScope,
+) : DragAndDropTargetStateInternal {
/**
- * Parses and returns the intent extra associated with the widget that is dropped into the grid.
- *
- * Returns null if the drop event didn't include intent information.
+ * The placeholder item that is treated as if it is being dragged across the grid. It is added
+ * to grid once drag and drop event is started and removed when event ends.
*/
- private fun DragAndDropEvent.maybeWidgetExtra(): WidgetPickerIntentUtils.WidgetExtra? {
- val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
- return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) }
+ private var placeHolder = CommunalContentModel.WidgetPlaceholder()
+ private var placeHolderIndex: Int? = null
+ private var previousTargetItemKey: Any? = null
+ private var dragOffset = Offset.Zero
+ private var columnWidth = 0
+
+ private val scrollChannel = Channel<Float>()
+
+ override suspend fun processScrollRequests(coroutineScope: CoroutineScope) {
+ while (true) {
+ val amount = scrollChannel.receive()
+
+ if (state.isScrollInProgress) {
+ // Ignore overscrolling if a scroll is already in progress (but we still want to
+ // consume the scroll event so that we don't end up processing a bunch of old
+ // events after scrolling has finished).
+ continue
+ }
+
+ // Perform the rest of the drag operation after scrolling has finished (or immediately
+ // if there will be no scrolling).
+ if (amount != 0f) {
+ scope.launch {
+ state.animateScrollBy(amount, tween(delayMillis = 250, durationMillis = 1000))
+ performDragAction()
+ }
+ } else {
+ performDragAction()
+ }
+ }
+ }
+
+ override fun onStarted() {
+ // assume item will be added to the end.
+ contentListState.list.add(placeHolder)
+ placeHolderIndex = contentListState.list.size - 1
+
+ // Use the width of the first item as the column width.
+ columnWidth =
+ state.layoutInfo.visibleItemsInfo.first().size.width +
+ state.layoutInfo.beforeContentPadding +
+ state.layoutInfo.afterContentPadding
}
- private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) }
+ override fun onMoved(event: DragAndDropEvent) {
+ dragOffset = event.toOffset()
+ scrollChannel.trySend(computeAutoscroll(dragOffset))
+ }
+
+ override fun onDrop(event: DragAndDropEvent): Boolean {
+ return placeHolderIndex?.let { dropIndex ->
+ val widgetExtra = event.maybeWidgetExtra() ?: return false
+ val (componentName, user) = widgetExtra
+ if (componentName != null && user != null) {
+ // Placeholder isn't removed yet to allow the setting the right rank for items
+ // before adding in the new item.
+ contentListState.onSaveList(
+ newItemComponentName = componentName,
+ newItemUser = user,
+ newItemIndex = dropIndex,
+ )
+ return@let true
+ }
+ return false
+ } ?: false
+ }
+
+ override fun onEnded() {
+ placeHolderIndex = null
+ previousTargetItemKey = null
+ contentListState.list.remove(placeHolder)
+ }
+
+ override fun onExited() {
+ onEnded()
+ }
+
+ private fun performDragAction() {
+ val targetItem =
+ state.layoutInfo.visibleItemsInfo
+ .asSequence()
+ .filter { item -> contentListState.isItemEditable(item.index) }
+ .firstItemAtOffset(dragOffset - contentOffset)
+
+ if (
+ targetItem != null &&
+ (!communalWidgetResizing() || targetItem.key != previousTargetItemKey)
+ ) {
+ if (communalWidgetResizing()) {
+ // Keep track of the previous target item, to avoid rapidly oscillating between
+ // items if the target item doesn't visually move as a result of the index change.
+ // In this case, even after the index changes, we'd still be colliding with the
+ // element, so it would be selected as the target item the next time this function
+ // runs again, which would trigger us to revert the index change we recently made.
+ previousTargetItemKey = targetItem.key
+ }
+
+ val scrollToIndex =
+ if (targetItem.index == state.firstVisibleItemIndex) {
+ placeHolderIndex
+ } else if (placeHolderIndex == state.firstVisibleItemIndex) {
+ targetItem.index
+ } else {
+ null
+ }
+
+ if (scrollToIndex != null) {
+ scope.launch {
+ state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+ movePlaceholderTo(targetItem.index)
+ }
+ } else {
+ movePlaceholderTo(targetItem.index)
+ }
+
+ placeHolderIndex = targetItem.index
+ } else if (targetItem == null) {
+ previousTargetItemKey = null
+ }
+ }
+
+ private fun computeAutoscroll(dragOffset: Offset): Float {
+ val orientation = state.layoutInfo.orientation
+ val distanceFromStart =
+ if (orientation == Orientation.Horizontal) {
+ dragOffset.x
+ } else {
+ dragOffset.y
+ }
+ val distanceFromEnd =
+ if (orientation == Orientation.Horizontal) {
+ state.layoutInfo.viewportEndOffset - dragOffset.x
+ } else {
+ state.layoutInfo.viewportEndOffset - dragOffset.y
+ }
+
+ return when {
+ distanceFromEnd < autoScrollThreshold -> {
+ (columnWidth - state.layoutInfo.beforeContentPadding).toFloat()
+ }
+ distanceFromStart < autoScrollThreshold -> {
+ -(columnWidth - state.layoutInfo.afterContentPadding).toFloat()
+ }
+ else -> 0f
+ }
+ }
+
+ private fun movePlaceholderTo(index: Int) {
+ val currentIndex = contentListState.list.indexOf(placeHolder)
+ if (currentIndex != index) {
+ contentListState.swapItems(currentIndex, index)
+ }
+ }
}
+
+/**
+ * Parses and returns the intent extra associated with the widget that is dropped into the grid.
+ *
+ * Returns null if the drop event didn't include intent information.
+ */
+private fun DragAndDropEvent.maybeWidgetExtra(): WidgetPickerIntentUtils.WidgetExtra? {
+ val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
+ return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) }
+}
+
+private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
index c972d3e3cf15..2a5addeb4951 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -19,7 +19,10 @@ package com.android.systemui.communal.ui.compose
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
@@ -37,13 +40,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import com.android.systemui.Flags.communalWidgetResizing
+import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
@@ -62,22 +68,22 @@ fun rememberGridDragDropState(
contentListState: ContentListState,
updateDragPositionForRemove: (boundingBox: IntRect) -> Boolean,
): GridDragDropState {
- val scope = rememberCoroutineScope()
+ val coroutineScope = rememberCoroutineScope()
+ val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
+
val state =
remember(gridState, contentListState, updateDragPositionForRemove) {
GridDragDropState(
- state = gridState,
+ gridState = gridState,
contentListState = contentListState,
- scope = scope,
+ coroutineScope = coroutineScope,
+ autoScrollThreshold = autoScrollThreshold,
updateDragPositionForRemove = updateDragPositionForRemove,
)
}
- LaunchedEffect(state) {
- while (true) {
- val diff = state.scrollChannel.receive()
- gridState.scrollBy(diff)
- }
- }
+
+ LaunchedEffect(state) { state.processScrollRequests(coroutineScope) }
+
return state
}
@@ -89,36 +95,86 @@ fun rememberGridDragDropState(
* to remove the dragged item if condition met and call [ContentListState.onSaveList] to persist any
* change in ordering.
*/
-class GridDragDropState
-internal constructor(
- private val state: LazyGridState,
- private val contentListState: ContentListState,
- private val scope: CoroutineScope,
+class GridDragDropState(
+ val gridState: LazyGridState,
+ contentListState: ContentListState,
+ coroutineScope: CoroutineScope,
+ autoScrollThreshold: Float,
private val updateDragPositionForRemove: (draggingBoundingBox: IntRect) -> Boolean,
) {
- var draggingItemKey by mutableStateOf<String?>(null)
- private set
+ private val dragDropState: GridDragDropStateInternal =
+ if (glanceableHubV2()) {
+ GridDragDropStateV2(
+ gridState = gridState,
+ contentListState = contentListState,
+ scope = coroutineScope,
+ autoScrollThreshold = autoScrollThreshold,
+ updateDragPositionForRemove = updateDragPositionForRemove,
+ )
+ } else {
+ GridDragDropStateV1(
+ gridState = gridState,
+ contentListState = contentListState,
+ scope = coroutineScope,
+ updateDragPositionForRemove = updateDragPositionForRemove,
+ )
+ }
- var isDraggingToRemove by mutableStateOf(false)
- private set
+ val draggingItemKey: String?
+ get() = dragDropState.draggingItemKey
- internal val scrollChannel = Channel<Float>()
+ val isDraggingToRemove: Boolean
+ get() = dragDropState.isDraggingToRemove
- private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
- private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
+ val draggingItemOffset: Offset
+ get() = dragDropState.draggingItemOffset
- private val spacer = CommunalContentModel.Spacer(CommunalContentSize.Responsive(1))
- private var spacerIndex: Int? = null
+ /**
+ * Called when dragging is initiated.
+ *
+ * @return {@code True} if dragging a grid item, {@code False} otherwise.
+ */
+ fun onDragStart(
+ offset: Offset,
+ screenWidth: Int,
+ layoutDirection: LayoutDirection,
+ contentOffset: Offset,
+ ): Boolean = dragDropState.onDragStart(offset, screenWidth, layoutDirection, contentOffset)
- private var previousTargetItemKey: Any? = null
+ fun onDragInterrupted() = dragDropState.onDragInterrupted()
+
+ fun onDrag(offset: Offset, layoutDirection: LayoutDirection) =
+ dragDropState.onDrag(offset, layoutDirection)
+
+ suspend fun processScrollRequests(coroutineScope: CoroutineScope) =
+ dragDropState.processScrollRequests(coroutineScope)
+}
+
+/**
+ * A private base class defining the API for handling drag-and-drop operations. There will be two
+ * implementations of this class: V1 for devices that do not have the glanceable_hub_v2 flag
+ * enabled, and V2 for devices that do have that flag enabled.
+ *
+ * TODO(b/400789179): Remove this class and the V1 implementation once glanceable_hub_v2 has
+ * shipped.
+ */
+private open class GridDragDropStateInternal(protected val state: LazyGridState) {
+ var draggingItemKey by mutableStateOf<String?>(null)
+ protected set
- internal val draggingItemOffset: Offset
+ var isDraggingToRemove by mutableStateOf(false)
+ protected set
+
+ var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
+ var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
+
+ val draggingItemOffset: Offset
get() =
draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
} ?: Offset.Zero
- private val draggingItemLayoutInfo: LazyGridItemInfo?
+ val draggingItemLayoutInfo: LazyGridItemInfo?
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == draggingItemKey }
/**
@@ -126,7 +182,45 @@ internal constructor(
*
* @return {@code True} if dragging a grid item, {@code False} otherwise.
*/
- internal fun onDragStart(
+ open fun onDragStart(
+ offset: Offset,
+ screenWidth: Int,
+ layoutDirection: LayoutDirection,
+ contentOffset: Offset,
+ ): Boolean = false
+
+ open fun onDragInterrupted() = Unit
+
+ open fun onDrag(offset: Offset, layoutDirection: LayoutDirection) = Unit
+
+ open suspend fun processScrollRequests(coroutineScope: CoroutineScope) = Unit
+}
+
+/**
+ * The V1 implementation of GridDragDropStateInternal to be used when the glanceable_hub_v2 flag is
+ * disabled.
+ */
+private class GridDragDropStateV1(
+ val gridState: LazyGridState,
+ private val contentListState: ContentListState,
+ private val scope: CoroutineScope,
+ private val updateDragPositionForRemove: (draggingBoundingBox: IntRect) -> Boolean,
+) : GridDragDropStateInternal(gridState) {
+ private val scrollChannel = Channel<Float>()
+
+ private val spacer = CommunalContentModel.Spacer(CommunalContentSize.Responsive(1))
+ private var spacerIndex: Int? = null
+
+ private var previousTargetItemKey: Any? = null
+
+ override suspend fun processScrollRequests(coroutineScope: CoroutineScope) {
+ while (true) {
+ val diff = scrollChannel.receive()
+ state.scrollBy(diff)
+ }
+ }
+
+ override fun onDragStart(
offset: Offset,
screenWidth: Int,
layoutDirection: LayoutDirection,
@@ -162,7 +256,7 @@ internal constructor(
return false
}
- internal fun onDragInterrupted() {
+ override fun onDragInterrupted() {
draggingItemKey?.let {
if (isDraggingToRemove) {
contentListState.onRemove(
@@ -185,7 +279,7 @@ internal constructor(
}
}
- internal fun onDrag(offset: Offset, layoutDirection: LayoutDirection) {
+ override fun onDrag(offset: Offset, layoutDirection: LayoutDirection) {
// Adjust offset to match the layout direction
draggingItemDraggedDelta +=
Offset(offset.x.directional(LayoutDirection.Ltr, layoutDirection), offset.y)
@@ -282,6 +376,249 @@ internal constructor(
}
}
+/**
+ * The V2 implementation of GridDragDropStateInternal to be used when the glanceable_hub_v2 flag is
+ * enabled.
+ */
+private class GridDragDropStateV2(
+ val gridState: LazyGridState,
+ private val contentListState: ContentListState,
+ private val scope: CoroutineScope,
+ private val autoScrollThreshold: Float,
+ private val updateDragPositionForRemove: (draggingBoundingBox: IntRect) -> Boolean,
+) : GridDragDropStateInternal(gridState) {
+
+ private val scrollChannel = Channel<Float>(Channel.UNLIMITED)
+
+ // Used to keep track of the dragging item during scrolling (because it might be off screen
+ // and no longer in the list of visible items).
+ private var draggingItemWhileScrolling: LazyGridItemInfo? by mutableStateOf(null)
+
+ private val spacer = CommunalContentModel.Spacer(CommunalContentSize.Responsive(1))
+ private var spacerIndex: Int? = null
+
+ private var previousTargetItemKey: Any? = null
+
+ // Basically, the location of the user's finger on the screen.
+ private var currentDragPositionOnScreen by mutableStateOf(Offset.Zero)
+ // The offset of the grid from the top of the screen.
+ private var contentOffset = Offset.Zero
+
+ // The width of one column in the grid (needed in order to auto-scroll one column at a time).
+ private var columnWidth = 0
+
+ override suspend fun processScrollRequests(coroutineScope: CoroutineScope) {
+ while (true) {
+ val amount = scrollChannel.receive()
+
+ if (state.isScrollInProgress) {
+ // Ignore overscrolling if a scroll is already in progress (but we still want to
+ // consume the scroll event so that we don't end up processing a bunch of old
+ // events after scrolling has finished).
+ continue
+ }
+
+ // We perform the rest of the drag action after scrolling has finished (or immediately
+ // if there will be no scrolling).
+ if (amount != 0f) {
+ coroutineScope.launch {
+ state.animateScrollBy(amount, tween(delayMillis = 250, durationMillis = 1000))
+ performDragAction()
+ }
+ } else {
+ performDragAction()
+ }
+ }
+ }
+
+ override fun onDragStart(
+ offset: Offset,
+ screenWidth: Int,
+ layoutDirection: LayoutDirection,
+ contentOffset: Offset,
+ ): Boolean {
+ val normalizedOffset =
+ Offset(
+ if (layoutDirection == LayoutDirection.Ltr) offset.x else screenWidth - offset.x,
+ offset.y,
+ )
+
+ currentDragPositionOnScreen = normalizedOffset
+ this.contentOffset = contentOffset
+
+ state.layoutInfo.visibleItemsInfo
+ .filter { item -> contentListState.isItemEditable(item.index) }
+ // grid item offset is based off grid content container so we need to deduct
+ // before content padding from the initial pointer position
+ .firstItemAtOffset(normalizedOffset - contentOffset)
+ ?.apply {
+ draggingItemKey = key as String
+ draggingItemWhileScrolling = this
+ draggingItemInitialOffset = this.offset.toOffset()
+ columnWidth =
+ this.size.width +
+ state.layoutInfo.beforeContentPadding +
+ state.layoutInfo.afterContentPadding
+ // Add a spacer after the last widget if it is larger than the dragging widget.
+ // This allows overscrolling, enabling the dragging widget to be placed beyond it.
+ val lastWidget = contentListState.list.lastOrNull { it.isWidgetContent() }
+ if (
+ lastWidget != null &&
+ draggingItemLayoutInfo != null &&
+ lastWidget.size.span > draggingItemLayoutInfo!!.span
+ ) {
+ contentListState.list.add(spacer)
+ spacerIndex = contentListState.list.size - 1
+ }
+ return true
+ }
+
+ return false
+ }
+
+ override fun onDragInterrupted() {
+ draggingItemKey?.let {
+ if (isDraggingToRemove) {
+ contentListState.onRemove(
+ contentListState.list.indexOfFirst { it.key == draggingItemKey }
+ )
+ isDraggingToRemove = false
+ updateDragPositionForRemove(IntRect.Zero)
+ }
+ // persist list editing changes on dragging ends
+ contentListState.onSaveList()
+ draggingItemKey = null
+ }
+ previousTargetItemKey = null
+ draggingItemDraggedDelta = Offset.Zero
+ draggingItemInitialOffset = Offset.Zero
+ currentDragPositionOnScreen = Offset.Zero
+ draggingItemWhileScrolling = null
+ // Remove spacer, if any, when a drag gesture finishes.
+ spacerIndex?.let {
+ contentListState.list.removeAt(it)
+ spacerIndex = null
+ }
+ }
+
+ override fun onDrag(offset: Offset, layoutDirection: LayoutDirection) {
+ // Adjust offset to match the layout direction
+ val delta = Offset(offset.x.directional(LayoutDirection.Ltr, layoutDirection), offset.y)
+ draggingItemDraggedDelta += delta
+ currentDragPositionOnScreen += delta
+
+ scrollChannel.trySend(computeAutoscroll(currentDragPositionOnScreen))
+ }
+
+ fun performDragAction() {
+ val draggingItem = draggingItemLayoutInfo ?: draggingItemWhileScrolling
+ if (draggingItem == null) {
+ return
+ }
+
+ val draggingBoundingBox =
+ IntRect(draggingItem.offset + draggingItemOffset.round(), draggingItem.size)
+ val curDragPositionInGrid = (currentDragPositionOnScreen - contentOffset)
+
+ val targetItem =
+ if (communalWidgetResizing()) {
+ val lastVisibleItemIndex = state.layoutInfo.visibleItemsInfo.last().index
+ state.layoutInfo.visibleItemsInfo.findLast(
+ fun(item): Boolean {
+ val itemBoundingBox = IntRect(item.offset, item.size)
+ return draggingItemKey != item.key &&
+ contentListState.isItemEditable(item.index) &&
+ itemBoundingBox.contains(curDragPositionInGrid.round()) &&
+ // If we swap with the last visible item, and that item doesn't fit
+ // in the gap created by moving the current item, then the current item
+ // will get placed after the last visible item. In this case, it gets
+ // placed outside of the viewport. We avoid this here, so the user
+ // has to scroll first before the swap can happen.
+ (item.index != lastVisibleItemIndex || item.span <= draggingItem.span)
+ }
+ )
+ } else {
+ state.layoutInfo.visibleItemsInfo
+ .asSequence()
+ .filter { item -> contentListState.isItemEditable(item.index) }
+ .filter { item -> draggingItem.index != item.index }
+ .firstItemAtOffset(curDragPositionInGrid)
+ }
+
+ if (
+ targetItem != null &&
+ (!communalWidgetResizing() || targetItem.key != previousTargetItemKey)
+ ) {
+ val scrollToIndex =
+ if (targetItem.index == state.firstVisibleItemIndex) {
+ draggingItem.index
+ } else if (draggingItem.index == state.firstVisibleItemIndex) {
+ targetItem.index
+ } else {
+ null
+ }
+ if (communalWidgetResizing()) {
+ // Keep track of the previous target item, to avoid rapidly oscillating between
+ // items if the target item doesn't visually move as a result of the index change.
+ // In this case, even after the index changes, we'd still be colliding with the
+ // element, so it would be selected as the target item the next time this function
+ // runs again, which would trigger us to revert the index change we recently made.
+ previousTargetItemKey = targetItem.key
+ }
+ if (scrollToIndex != null) {
+ scope.launch {
+ // this is needed to neutralize automatic keeping the first item first.
+ state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+ contentListState.swapItems(draggingItem.index, targetItem.index)
+ }
+ } else {
+ contentListState.swapItems(draggingItem.index, targetItem.index)
+ }
+ draggingItemWhileScrolling = targetItem
+ isDraggingToRemove = false
+ } else if (targetItem == null) {
+ isDraggingToRemove = checkForRemove(draggingBoundingBox)
+ previousTargetItemKey = null
+ }
+ }
+
+ /** Calculate the amount dragged out of bound on both sides. Returns 0f if not overscrolled. */
+ private fun computeAutoscroll(dragOffset: Offset): Float {
+ val orientation = state.layoutInfo.orientation
+ val distanceFromStart =
+ if (orientation == Orientation.Horizontal) {
+ dragOffset.x
+ } else {
+ dragOffset.y
+ }
+ val distanceFromEnd =
+ if (orientation == Orientation.Horizontal) {
+ state.layoutInfo.viewportEndOffset - dragOffset.x
+ } else {
+ state.layoutInfo.viewportEndOffset - dragOffset.y
+ }
+
+ return when {
+ distanceFromEnd < autoScrollThreshold -> {
+ (columnWidth - state.layoutInfo.beforeContentPadding).toFloat()
+ }
+ distanceFromStart < autoScrollThreshold -> {
+ -(columnWidth - state.layoutInfo.afterContentPadding).toFloat()
+ }
+ else -> 0f
+ }
+ }
+
+ /** Calls the callback with the updated drag position and returns whether to remove the item. */
+ private fun checkForRemove(draggingItemBoundingBox: IntRect): Boolean {
+ return if (draggingItemDraggedDelta.y < 0) {
+ updateDragPositionForRemove(draggingItemBoundingBox)
+ } else {
+ false
+ }
+ }
+}
+
fun Modifier.dragContainer(
dragDropState: GridDragDropState,
layoutDirection: LayoutDirection,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 046d92d58978..2ab36501d87d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -443,4 +443,24 @@ class FromAodTransitionInteractorTest : SysuiTestCase() {
Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal)
assertThat(transitionRepository).noTransitionsStarted()
}
+
+ @Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun testDoNotTransitionToGlanceableHub_onWakeUpFromAodDueToMotion() =
+ kosmos.runTest {
+ setCommunalV2Available(true)
+
+ val currentScene by collectLastValue(communalSceneInteractor.currentScene)
+ fakeCommunalSceneRepository.changeScene(CommunalScenes.Blank)
+
+ // Communal is not showing
+ Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank)
+
+ powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT)
+ testScope.advanceTimeBy(100) // account for debouncing
+
+ Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank)
+ assertThat(transitionRepository)
+ .startedTransition(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 096c3dafd01c..c3d18a3d893c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -20,7 +20,6 @@ import android.os.PowerManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
-import android.provider.Settings
import android.service.dream.dreamManager
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
@@ -30,8 +29,6 @@ import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.data.repository.batteryRepository
-import com.android.systemui.common.data.repository.fake
import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
@@ -61,8 +58,6 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.flow.flowOf
@@ -171,15 +166,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
fun testTransitionToLockscreen_onWake_canDream_ktfRefactor() =
kosmos.runTest {
setCommunalAvailable(true)
- if (glanceableHubV2()) {
- val user = fakeUserRepository.asMainUser()
- fakeSettings.putIntForUser(
- Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
- 1,
- user.id,
- )
- batteryRepository.fake.setDevicePluggedIn(true)
- } else {
+ if (!glanceableHubV2()) {
whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
}
@@ -226,15 +213,7 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
fun testTransitionToGlanceableHub_onWakeup_ifAvailable() =
kosmos.runTest {
setCommunalAvailable(true)
- if (glanceableHubV2()) {
- val user = fakeUserRepository.asMainUser()
- fakeSettings.putIntForUser(
- Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
- 1,
- user.id,
- )
- batteryRepository.fake.setDevicePluggedIn(true)
- } else {
+ if (!glanceableHubV2()) {
whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
}
@@ -250,6 +229,25 @@ class FromDozingTransitionInteractorTest(flags: FlagsParameterization?) : SysuiT
}
@Test
+ @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR, FLAG_SCENE_CONTAINER)
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ fun testTransitionToLockscreen_onWakeupFromLift() =
+ kosmos.runTest {
+ setCommunalAvailable(true)
+ if (!glanceableHubV2()) {
+ whenever(dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ }
+
+ // Device turns on.
+ powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LIFT)
+ testScope.advanceTimeBy(51L)
+
+ // We transition to the lockscreen instead of the hub.
+ assertThat(transitionRepository)
+ .startedTransition(from = KeyguardState.DOZING, to = KeyguardState.LOCKSCREEN)
+ }
+
+ @Test
@EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionToOccluded_onWakeup_whenOccludingActivityOnTop() =
kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt
new file mode 100644
index 000000000000..1adba6fcd45d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ModesDndTileTest.kt
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles
+
+import android.app.Flags
+import android.os.Handler
+import android.platform.test.annotations.EnableFlags
+import android.service.quicksettings.Tile
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
+import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
+
+@EnableFlags(Flags.FLAG_MODES_UI)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper(setAsMainLooper = true)
+class ModesDndTileTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val testDispatcher = kosmos.testDispatcher
+
+ @Mock private lateinit var qsHost: QSHost
+
+ @Mock private lateinit var metricsLogger: MetricsLogger
+
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ @Mock private lateinit var activityStarter: ActivityStarter
+
+ @Mock private lateinit var qsLogger: QSLogger
+
+ @Mock private lateinit var uiEventLogger: QsEventLogger
+
+ @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider
+
+ @Mock private lateinit var dialogDelegate: ModesDialogDelegate
+
+ @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository
+
+ private val inputHandler = FakeQSTileIntentUserInputHandler()
+ private val zenModeRepository = kosmos.zenModeRepository
+ private val tileDataInteractor =
+ ModesDndTileDataInteractor(context, kosmos.zenModeInteractor, testDispatcher)
+ private val mapper = ModesDndTileMapper(context.resources, context.theme)
+
+ private lateinit var userActionInteractor: ModesDndTileUserActionInteractor
+ private lateinit var secureSettings: SecureSettings
+ private lateinit var testableLooper: TestableLooper
+ private lateinit var underTest: ModesDndTile
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+ secureSettings = FakeSettings()
+
+ // Allow the tile to load resources
+ whenever(qsHost.context).thenReturn(context)
+ whenever(qsHost.userContext).thenReturn(context)
+
+ whenever(qsTileConfigProvider.getConfig(any()))
+ .thenReturn(
+ QSTileConfigTestBuilder.build {
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_dnd_icon_off,
+ labelRes = R.string.quick_settings_dnd_label,
+ )
+ }
+ )
+
+ userActionInteractor =
+ ModesDndTileUserActionInteractor(
+ kosmos.mainCoroutineContext,
+ inputHandler,
+ dialogDelegate,
+ kosmos.zenModeInteractor,
+ kosmos.modesDialogEventLogger,
+ settingsPackageRepository,
+ )
+
+ underTest =
+ ModesDndTile(
+ qsHost,
+ uiEventLogger,
+ testableLooper.looper,
+ Handler(testableLooper.looper),
+ FalsingManagerFake(),
+ metricsLogger,
+ statusBarStateController,
+ activityStarter,
+ qsLogger,
+ qsTileConfigProvider,
+ tileDataInteractor,
+ mapper,
+ userActionInteractor,
+ )
+
+ underTest.initialize()
+ underTest.setListening(Object(), true)
+
+ testableLooper.processAllMessages()
+ }
+
+ @After
+ fun tearDown() {
+ underTest.destroy()
+ testableLooper.processAllMessages()
+ }
+
+ @Test
+ fun stateUpdatesOnChange() =
+ testScope.runTest {
+ assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)
+
+ zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND)
+ runCurrent()
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.state.state).isEqualTo(Tile.STATE_ACTIVE)
+ }
+
+ @Test
+ fun handleUpdateState_withModel_updatesState() =
+ testScope.runTest {
+ val tileState =
+ BooleanState().apply {
+ state = Tile.STATE_INACTIVE
+ secondaryLabel = "Old secondary label"
+ }
+ val model = ModesDndTileModel(isActivated = true)
+
+ underTest.handleUpdateState(tileState, model)
+
+ assertThat(tileState.state).isEqualTo(Tile.STATE_ACTIVE)
+ assertThat(tileState.secondaryLabel).isEqualTo("On")
+ }
+
+ @Test
+ fun handleUpdateState_withNull_updatesState() =
+ testScope.runTest {
+ val tileState =
+ BooleanState().apply {
+ state = Tile.STATE_INACTIVE
+ secondaryLabel = "Old secondary label"
+ }
+ zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND)
+ runCurrent()
+
+ underTest.handleUpdateState(tileState, null)
+
+ assertThat(tileState.state).isEqualTo(Tile.STATE_ACTIVE)
+ assertThat(tileState.secondaryLabel).isEqualTo("On")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt
new file mode 100644
index 000000000000..23d7b86df875
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractorTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import android.app.Flags
+import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+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.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@EnableFlags(Flags.FLAG_MODES_UI)
+@RunWith(AndroidJUnit4::class)
+class ModesDndTileDataInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val dispatcher = kosmos.testDispatcher
+ private val zenModeRepository = kosmos.fakeZenModeRepository
+
+ private val underTest by lazy {
+ ModesDndTileDataInteractor(context, kosmos.zenModeInteractor, dispatcher)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_UI_DND_TILE)
+ fun availability_flagOn_isTrue() =
+ testScope.runTest {
+ val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+
+ assertThat(availability).containsExactly(true)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_UI_DND_TILE)
+ fun availability_flagOff_isFalse() =
+ testScope.runTest {
+ val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())
+
+ assertThat(availability).containsExactly(false)
+ }
+
+ @Test
+ fun tileData_dndChanges_updateActivated() =
+ testScope.runTest {
+ val model by
+ collectLastValue(
+ underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ runCurrent()
+ assertThat(model!!.isActivated).isFalse()
+
+ zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND)
+ runCurrent()
+ assertThat(model!!.isActivated).isTrue()
+
+ zenModeRepository.deactivateMode(TestModeBuilder.MANUAL_DND)
+ runCurrent()
+ assertThat(model!!.isActivated).isFalse()
+ }
+
+ @Test
+ fun tileData_otherModeChanges_notActivated() =
+ testScope.runTest {
+ val model by
+ collectLastValue(
+ underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ runCurrent()
+ assertThat(model!!.isActivated).isFalse()
+
+ zenModeRepository.addMode("Other mode")
+ runCurrent()
+ assertThat(model!!.isActivated).isFalse()
+
+ zenModeRepository.activateMode("Other mode")
+ runCurrent()
+ assertThat(model!!.isActivated).isFalse()
+ }
+
+ private companion object {
+ val TEST_USER = UserHandle.of(1)!!
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt
new file mode 100644
index 000000000000..0a35b428bbc9
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractorTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate
+import com.android.systemui.statusbar.policy.ui.dialog.modesDialogEventLogger
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(android.app.Flags.FLAG_MODES_UI)
+class ModesDndTileUserActionInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val inputHandler = kosmos.qsTileIntentUserInputHandler
+ private val mockDialogDelegate = kosmos.mockModesDialogDelegate
+ private val zenModeRepository = kosmos.zenModeRepository
+ private val zenModeInteractor = kosmos.zenModeInteractor
+ private val settingsPackageRepository = mock<QSSettingsPackageRepository>()
+
+ private val underTest =
+ ModesDndTileUserActionInteractor(
+ kosmos.mainCoroutineContext,
+ inputHandler,
+ mockDialogDelegate,
+ zenModeInteractor,
+ kosmos.modesDialogEventLogger,
+ settingsPackageRepository,
+ )
+
+ @Before
+ fun setUp() {
+ whenever(settingsPackageRepository.getSettingsPackageName()).thenReturn(SETTINGS_PACKAGE)
+ }
+
+ @Test
+ fun handleClick_dndActive_deactivatesDnd() =
+ testScope.runTest {
+ val dndMode by collectLastValue(zenModeInteractor.dndMode)
+ zenModeRepository.activateMode(MANUAL_DND)
+ assertThat(dndMode?.isActive).isTrue()
+
+ underTest.handleInput(QSTileInputTestKtx.click(data = ModesDndTileModel(true)))
+
+ assertThat(dndMode?.isActive).isFalse()
+ }
+
+ @Test
+ fun handleClick_dndInactive_activatesDnd() =
+ testScope.runTest {
+ val dndMode by collectLastValue(zenModeInteractor.dndMode)
+ assertThat(dndMode?.isActive).isFalse()
+
+ underTest.handleInput(QSTileInputTestKtx.click(data = ModesDndTileModel(false)))
+
+ assertThat(dndMode?.isActive).isTrue()
+ }
+
+ @Test
+ fun handleLongClick_active_opensSettings() =
+ testScope.runTest {
+ zenModeRepository.activateMode(MANUAL_DND)
+ runCurrent()
+
+ underTest.handleInput(QSTileInputTestKtx.longClick(ModesDndTileModel(true)))
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE)
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+ assertThat(it.intent.getStringExtra(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID))
+ .isEqualTo(MANUAL_DND.id)
+ }
+ }
+
+ @Test
+ fun handleLongClick_inactive_opensSettings() =
+ testScope.runTest {
+ zenModeRepository.activateMode(MANUAL_DND)
+ zenModeRepository.deactivateMode(MANUAL_DND)
+ runCurrent()
+
+ underTest.handleInput(QSTileInputTestKtx.longClick(ModesDndTileModel(false)))
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE)
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+ assertThat(it.intent.getStringExtra(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID))
+ .isEqualTo(MANUAL_DND.id)
+ }
+ }
+
+ companion object {
+ private const val SETTINGS_PACKAGE = "the.settings.package"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt
new file mode 100644
index 000000000000..29f642a4325d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapperTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.ui
+
+import android.app.Flags
+import android.graphics.drawable.TestStubDrawable
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+class ModesDndTileMapperTest : SysuiTestCase() {
+ val config =
+ QSTileConfigTestBuilder.build {
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_dnd_icon_off,
+ labelRes = R.string.quick_settings_modes_label,
+ )
+ }
+
+ val underTest =
+ ModesDndTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
+ addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
+ }
+ .resources,
+ context.theme,
+ )
+
+ @Test
+ fun map_inactiveState() {
+ val model = ModesDndTileModel(isActivated = false)
+
+ val state = underTest.map(config, model)
+
+ assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
+ assertThat((state.icon as Icon.Loaded).res).isEqualTo(R.drawable.qs_dnd_icon_off)
+ assertThat(state.secondaryLabel).isEqualTo("Off")
+ }
+
+ @Test
+ fun map_activeState() {
+ val model = ModesDndTileModel(isActivated = true)
+
+ val state = underTest.map(config, model)
+
+ assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+ assertThat((state.icon as Icon.Loaded).res).isEqualTo(R.drawable.qs_dnd_icon_on)
+ assertThat(state.secondaryLabel).isEqualTo("On")
+ }
+}
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 72b003f9f463..998a7ea805a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -33,7 +33,7 @@ import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
@@ -693,7 +693,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
mLockscreenUserManager.mConnectedToWifi.set(false);
// Sensitive Content notifications are always redacted
- assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT,
+ assertEquals(REDACTION_TYPE_OTP,
mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index 65763a359c0f..f9405af3f85d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -16,7 +16,9 @@
package com.android.systemui.statusbar.notification.collection.coordinator;
+import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
@@ -340,6 +342,13 @@ public class RankingCoordinatorTest extends SysuiTestCase {
}
@Test
+ public void testAlertingSectioner_rejectsBundle() {
+ for (String id : SYSTEM_RESERVED_IDS) {
+ assertFalse(mAlertingSectioner.isInSection(makeClassifiedNotifEntry(id)));
+ }
+ }
+
+ @Test
public void statusBarStateCallbackTest() {
mStatusBarStateCallback.onDozeAmountChanged(1f, 1f);
verify(mInvalidationListener, times(1))
@@ -392,4 +401,11 @@ public class RankingCoordinatorTest extends SysuiTestCase {
.build());
assertEquals(ambient, mEntry.getRanking().isAmbient());
}
+
+ private NotificationEntry makeClassifiedNotifEntry(String channelId) {
+ NotificationChannel channel = new NotificationChannel(channelId, channelId, IMPORTANCE_LOW);
+ return new NotificationEntryBuilder()
+ .updateRanking((rankingBuilder -> rankingBuilder.setChannel(channel)))
+ .build();
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 9f35d631bd45..99f2596dbf1d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.notification.row;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL;
@@ -472,7 +472,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
com.android.systemui.res.R.drawable.ic_person).setStyle(messagingStyle).build();
ExpandableNotificationRow row = mHelper.createRow(messageNotif);
inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC,
- REDACTION_TYPE_SENSITIVE_CONTENT, row);
+ REDACTION_TYPE_OTP, row);
NotificationContentView publicView = row.getPublicLayout();
assertNotNull(publicView);
// The display name should be included, but not the content or message text
@@ -493,7 +493,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase {
.build();
ExpandableNotificationRow row = mHelper.createRow(notif);
inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC,
- REDACTION_TYPE_SENSITIVE_CONTENT, row);
+ REDACTION_TYPE_OTP, row);
NotificationContentView publicView = row.getPublicLayout();
assertNotNull(publicView);
assertFalse(hasText(publicView, contentText));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 31413b06e5fd..063a04ab9f37 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -36,8 +36,8 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP
import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC
-import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT
import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
@@ -538,7 +538,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
false,
notificationInflater,
FLAG_CONTENT_VIEW_PUBLIC,
- REDACTION_TYPE_SENSITIVE_CONTENT,
+ REDACTION_TYPE_OTP,
newRow,
)
// The display name should be included, but not the content or message text
@@ -566,7 +566,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() {
false,
notificationInflater,
FLAG_CONTENT_VIEW_PUBLIC,
- REDACTION_TYPE_SENSITIVE_CONTENT,
+ REDACTION_TYPE_OTP,
newRow,
)
var publicView = newRow.publicLayout
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
index 531b30b9547a..0fb0548582ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
@@ -18,27 +18,27 @@ package com.android.systemui.statusbar.notification.shared
import com.google.common.truth.Correspondence
val byKey: Correspondence<ActiveNotificationModel, String> =
- Correspondence.transforming({ it?.key }, "has a key of")
+ Correspondence.transforming({ it.key }, "has a key of")
val byIsAmbient: Correspondence<ActiveNotificationModel, Boolean> =
- Correspondence.transforming({ it?.isAmbient }, "has an isAmbient value of")
+ Correspondence.transforming({ it.isAmbient }, "has an isAmbient value of")
val byIsSuppressedFromStatusBar: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming(
- { it?.isSuppressedFromStatusBar },
+ { it.isSuppressedFromStatusBar },
"has an isSuppressedFromStatusBar value of",
)
val byIsSilent: Correspondence<ActiveNotificationModel, Boolean> =
- Correspondence.transforming({ it?.isSilent }, "has an isSilent value of")
+ Correspondence.transforming({ it.isSilent }, "has an isSilent value of")
val byIsRowDismissed: Correspondence<ActiveNotificationModel, Boolean> =
- Correspondence.transforming({ it?.isRowDismissed }, "has an isRowDismissed value of")
+ Correspondence.transforming({ it.isRowDismissed }, "has an isRowDismissed value of")
val byIsLastMessageFromReply: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming(
- { it?.isLastMessageFromReply },
+ { it.isLastMessageFromReply },
"has an isLastMessageFromReply value of",
)
val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> =
- Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of")
+ Correspondence.transforming({ it.isPulsing }, "has an isPulsing value of")
val byIsPromoted: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming(
- { it?.promotedContent != null },
+ { it.promotedContent != null },
"has (or doesn't have) a promoted content model",
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 08ecbac1582c..41cca19346f0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -38,6 +38,7 @@ import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy
import com.android.systemui.testKosmos
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
@@ -1572,7 +1573,11 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
fullStackHeight: Float = 3000f,
) {
ambientState.headsUpTop = headsUpTop
- ambientState.headsUpBottom = headsUpBottom
+ if (NotificationsHunSharedAnimationValues.isEnabled) {
+ headsUpAnimator.headsUpAppearHeightBottom = headsUpBottom.roundToInt()
+ } else {
+ ambientState.headsUpBottom = headsUpBottom
+ }
ambientState.stackTop = stackTop
ambientState.stackCutoff = stackCutoff
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 78e719f6289a..549fdefd8f7a 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -115,7 +115,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false">
- internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects
+ internet,bt,flashlight,dnd,modes_dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects
</string>
<!-- The tiles to display in QuickSettings -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8c1fd65d96d4..cd94a265aa80 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3181,8 +3181,6 @@
<string name="controls_media_active_session">The current media session cannot be hidden.</string>
<!-- Label for a button that will hide media controls [CHAR_LIMIT=30] -->
<string name="controls_media_dismiss_button">Hide</string>
- <!-- Label for button to resume media playback [CHAR_LIMIT=NONE] -->
- <string name="controls_media_resume">Resume</string>
<!-- Label for button to go to media control settings screen [CHAR_LIMIT=30] -->
<string name="controls_media_settings_button">Settings</string>
<!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index faf06f3d39f0..bcd49b91d894 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -85,6 +85,16 @@
<item>On</item>
</string-array>
+ <!-- State names for dnd (Do not disturb) mode tile: unavailable, off, on.
+ This subtitle is shown when the tile is in that particular state but does not set its own
+ subtitle, so some of these may never appear on screen. They should still be translated as
+ if they could appear. [CHAR LIMIT=32] -->
+ <string-array name="tile_states_modes_dnd">
+ <item>Unavailable</item>
+ <item>Off</item>
+ <item>On</item>
+ </string-array>
+
<!-- State names for flashlight tile: unavailable, off, on.
This subtitle is shown when the tile is in that particular state but does not set its own
subtitle, so some of these may never appear on screen. They should still be translated as
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index 39f55803bb73..c4e1ccf6b62e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -31,7 +31,7 @@ import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.biometrics.shared.model.toSensorStrength
import com.android.systemui.biometrics.shared.model.toSensorType
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
index 230b30bc548e..cce33fdf16c1 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt
@@ -21,7 +21,7 @@ import android.util.Log
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt
index 40313e3158aa..6484116233ca 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt
@@ -21,7 +21,7 @@ import android.content.res.Configuration
import com.android.systemui.biometrics.data.repository.DisplayStateRepository
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.display.data.repository.DisplayRepository
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt
index 7f1cb5da474d..dea3c472a476 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothAutoOnRepository.kt
@@ -21,7 +21,7 @@ import android.util.Log
import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
index 55d4d3efbe27..9e0f10277197 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
@@ -22,7 +22,7 @@ import android.bluetooth.BluetoothAdapter.STATE_ON
import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index b606c19b3503..e458b8092cda 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -24,7 +24,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
index b9e1c55fbade..89208364178d 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt
@@ -28,7 +28,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.bouncer.data.model.SimBouncerModel
import com.android.systemui.bouncer.data.model.SimPukInputModel
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt
index e6d6293733d4..636b3ab66dd5 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt
@@ -24,7 +24,7 @@ import com.android.systemui.brightness.shared.model.BrightnessLog
import com.android.systemui.brightness.shared.model.LinearBrightness
import com.android.systemui.brightness.shared.model.formatBrightness
import com.android.systemui.brightness.shared.model.logDiffForTable
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
index 183a3cc26b95..724670d955dd 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
@@ -32,7 +32,7 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.BroadcastRunning
import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt b/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt
index 7816a1487c01..dac5b7efaade 100644
--- a/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/camera/data/repository/CameraSensorPrivacyRepository.kt
@@ -19,7 +19,7 @@ package com.android.systemui.camera.data.repository
import android.hardware.SensorPrivacyManager
import android.hardware.SensorPrivacyManager.Sensors.CAMERA
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
index ae89b39175c1..0d7a2d9707d7 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt
@@ -17,7 +17,7 @@
package com.android.systemui.communal.domain.interactor
import android.content.pm.UserInfo
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.communal.data.model.FEATURE_AUTO_OPEN
import com.android.systemui.communal.data.model.FEATURE_ENABLED
import com.android.systemui.communal.data.model.FEATURE_MANUAL_OPEN
diff --git a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt
index 6f579a3986c8..d7ffbb2e76b8 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/settings/ControlsSettingsRepositoryImpl.kt
@@ -18,7 +18,7 @@
package com.android.systemui.controls.settings
import android.provider.Settings
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
index 69378b475938..449a995b782a 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -27,7 +27,7 @@ import com.android.systemui.Dumpable
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
index 675f00a89d23..b7315cc994a8 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
@@ -1,7 +1,7 @@
package com.android.systemui.deviceentry.data.repository
import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt
index 29044d017d2d..f4db2cc71b38 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt
@@ -27,7 +27,7 @@ import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFI
import android.hardware.devicestate.DeviceStateManager
import android.hardware.devicestate.feature.flags.Flags as DeviceStateManagerFlags
import com.android.internal.R
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DeviceStateRepository.DeviceState
import java.util.concurrent.Executor
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
index cef45dcae43e..3c554b9ff66b 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayMetricsRepository.kt
@@ -19,7 +19,7 @@ package com.android.systemui.display.data.repository
import android.content.Context
import android.content.res.Configuration
import android.util.DisplayMetrics
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
diff --git a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt
index dc08570447a5..e5920924a4be 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/PluggedInCondition.kt
@@ -16,7 +16,7 @@
package com.android.systemui.flags
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.statusbar.policy.BatteryController
import dagger.Lazy
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
index 922bc15c0633..4e7164ff12d7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt
@@ -21,7 +21,7 @@ import android.hardware.input.InputManager.StickyModifierStateListener
import android.hardware.input.StickyModifierState
import android.provider.Settings
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
index 07ed194dd68f..40861929add7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt
@@ -30,7 +30,7 @@ import com.android.settingslib.notification.modes.EnableDndDialogFactory
import com.android.settingslib.notification.modes.EnableDndDialogMetricsLogger
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
index e2642a0964c1..683c11a88b89 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt
@@ -20,7 +20,7 @@ package com.android.systemui.keyguard.data.quickaffordance
import android.content.Context
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index 3555f06ce96f..a1dafb1438ca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -24,7 +24,7 @@ import androidx.annotation.DrawableRes
import com.android.systemui.res.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.controls.ControlsServiceInfo
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
index ad79177fdd76..01ff0e1344c6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
@@ -23,7 +23,7 @@ import android.content.SharedPreferences
import com.android.systemui.backup.BackupHelper
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
import com.android.systemui.settings.UserFileManager
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
index 1c9bc9f39663..10fc4c2a02ff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt
@@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.animation.Expandable
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index d12c42a754f0..7c33e29bf25a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -21,7 +21,7 @@ import android.content.Context
import com.android.systemui.res.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index 760adbf58d93..56ea26e88b23 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -26,7 +26,7 @@ import android.service.quickaccesswallet.WalletCard
import android.util.Log
import com.android.systemui.animation.Expandable
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index 0f5f31302670..30476b991baf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -35,7 +35,7 @@ import com.android.systemui.biometrics.data.repository.FingerprintPropertyReposi
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index 4d999df69588..396f60645f00 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -24,7 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt
index 7c430920cb46..59e6a08c4511 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DevicePostureRepository.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyguard.data.repository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.model.DevicePosture
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index affcd33b7170..cd0efdae337d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -24,7 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
index c5a6fa199c58..63a0286832d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
@@ -20,7 +20,7 @@ import android.app.trust.TrustManager
import com.android.keyguard.TrustGrantFlags
import com.android.keyguard.logging.TrustRepositoryLogger
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 54af8f5b9806..f53421d539fe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -101,14 +101,14 @@ constructor(
)
.collect {
(
- _,
+ detailedWakefulness,
startedStep,
canWakeDirectlyToGone,
) ->
val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
val biometricUnlockMode = keyguardInteractor.biometricUnlockState.value.mode
val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
- val shouldShowCommunal = communalSettingsInteractor.autoOpenEnabled.value
+ val autoOpenCommunal = communalSettingsInteractor.autoOpenEnabled.value
if (!maybeHandleInsecurePowerGesture()) {
val shouldTransitionToLockscreen =
@@ -135,8 +135,12 @@ constructor(
(!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen()) ||
(KeyguardWmStateRefactor.isEnabled && canWakeDirectlyToGone)
+ // Avoid transitioning to communal automatically if the device is waking
+ // up due to motion.
val shouldTransitionToCommunal =
- communalSettingsInteractor.isV2FlagEnabled() && shouldShowCommunal
+ communalSettingsInteractor.isV2FlagEnabled() &&
+ autoOpenCommunal &&
+ !detailedWakefulness.isAwakeFromMotionOrLift()
if (shouldTransitionToGone) {
// TODO(b/360368320): Adapt for scene framework
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 1fc41085f772..4aaa1fab4c65 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -34,8 +34,9 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakefulnessModel
import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.util.kotlin.Utils.Companion.sample
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -121,9 +122,10 @@ constructor(
private fun shouldTransitionToCommunal(
shouldShowCommunal: Boolean,
isCommunalAvailable: Boolean,
+ wakefulness: WakefulnessModel,
) =
if (communalSettingsInteractor.isV2FlagEnabled()) {
- shouldShowCommunal
+ shouldShowCommunal && !wakefulness.isAwakeFromMotionOrLift()
} else {
isCommunalAvailable && dreamManager.canStartDreaming(false)
}
@@ -148,14 +150,14 @@ constructor(
}
scope.launch {
- powerInteractor.isAwake
+ powerInteractor.detailedWakefulness
.debounce(50L)
- .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
- .sample(
+ .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() }
+ .sampleCombine(
communalInteractor.isCommunalAvailable,
communalSettingsInteractor.autoOpenEnabled,
)
- .collect { (_, isCommunalAvailable, shouldShowCommunal) ->
+ .collect { (detailedWakefulness, isCommunalAvailable, shouldShowCommunal) ->
val isKeyguardOccludedLegacy = keyguardInteractor.isKeyguardOccluded.value
val primaryBouncerShowing = keyguardInteractor.primaryBouncerShowing.value
val isKeyguardGoingAway = keyguardInteractor.isKeyguardGoingAway.value
@@ -186,7 +188,11 @@ constructor(
} else if (isKeyguardOccludedLegacy) {
startTransitionTo(KeyguardState.OCCLUDED)
} else if (
- shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable)
+ shouldTransitionToCommunal(
+ shouldShowCommunal,
+ isCommunalAvailable,
+ detailedWakefulness,
+ )
) {
if (!SceneContainerFlag.isEnabled) {
transitionToGlanceableHub()
@@ -208,7 +214,7 @@ constructor(
scope.launch {
powerInteractor.detailedWakefulness
.filterRelevantKeyguardStateAnd { it.isAwake() }
- .sample(
+ .sampleCombine(
communalSettingsInteractor.autoOpenEnabled,
communalInteractor.isCommunalAvailable,
keyguardInteractor.biometricUnlockState,
@@ -217,7 +223,7 @@ constructor(
)
.collect {
(
- _,
+ detailedWakefulness,
shouldShowCommunal,
isCommunalAvailable,
biometricUnlockState,
@@ -245,7 +251,11 @@ constructor(
)
}
} else if (
- shouldTransitionToCommunal(shouldShowCommunal, isCommunalAvailable)
+ shouldTransitionToCommunal(
+ shouldShowCommunal,
+ isCommunalAvailable,
+ detailedWakefulness,
+ )
) {
if (!SceneContainerFlag.isEnabled) {
transitionToGlanceableHub()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 309b6751176c..c2efc7559487 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -1256,7 +1256,7 @@ class LegacyMediaDataManagerImpl(
return MediaAction(
Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context),
action,
- context.getString(R.string.controls_media_resume),
+ context.getString(R.string.controls_media_button_play),
if (Flags.mediaControlsUiUpdate()) {
context.getDrawable(R.drawable.ic_media_play_button_container)
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index dbd2250a75b0..a7c5a36b804a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -521,7 +521,7 @@ constructor(
return MediaAction(
Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context),
action,
- context.getString(R.string.controls_media_resume),
+ context.getString(R.string.controls_media_button_play),
if (Flags.mediaControlsUiUpdate()) {
context.getDrawable(R.drawable.ic_media_play_button_container)
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 94df4b353c94..ca4a65953cba 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -1193,7 +1193,7 @@ class MediaDataProcessor(
return MediaAction(
Icon.createWithResource(context, iconId).setTint(themeText).loadDrawable(context),
action,
- context.getString(R.string.controls_media_resume),
+ context.getString(R.string.controls_media_button_play),
if (Flags.mediaControlsUiUpdate()) {
context.getDrawable(R.drawable.ic_media_play_button_container)
} else {
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 f79693138e24..d0c6a3e6a3ef 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -66,6 +66,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import com.android.internal.annotations.GuardedBy;
+import com.android.media.flags.Flags;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.BluetoothUtils;
@@ -78,7 +79,6 @@ import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.InputRouteManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.media.flags.Flags;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -226,7 +226,7 @@ public class MediaSwitchingController
InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
- mOutputMediaItemListProxy = new OutputMediaItemListProxy();
+ mOutputMediaItemListProxy = new OutputMediaItemListProxy(context);
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
@@ -308,7 +308,8 @@ public class MediaSwitchingController
}
private MediaController getMediaController() {
- if (mToken != null && Flags.usePlaybackInfoForRoutingControls()) {
+ if (mToken != null
+ && com.android.settingslib.media.flags.Flags.usePlaybackInfoForRoutingControls()) {
return new MediaController(mContext, mToken);
} else {
for (NotificationEntry entry : mNotifCollection.getAllNotifs()) {
@@ -577,19 +578,35 @@ public class MediaSwitchingController
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- List<MediaItem> updatedMediaItems =
- buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices);
- mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems);
+ if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
+ attachRangeInfo(devices);
+ Collections.sort(devices, Comparator.naturalOrder());
+ }
+ if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) {
+ // For the first time building list, to make sure the top device is the connected
+ // device.
+ boolean needToHandleMutingExpectedDevice =
+ hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote();
+ final MediaDevice connectedMediaDevice =
+ needToHandleMutingExpectedDevice ? null : getCurrentConnectedMediaDevice();
+ mOutputMediaItemListProxy.updateMediaDevices(
+ devices,
+ getSelectedMediaDevice(),
+ connectedMediaDevice,
+ needToHandleMutingExpectedDevice,
+ getConnectNewDeviceItem());
+ } else {
+ List<MediaItem> updatedMediaItems =
+ buildMediaItems(
+ mOutputMediaItemListProxy.getOutputMediaItemList(), devices);
+ mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems);
+ }
}
}
protected List<MediaItem> buildMediaItems(
List<MediaItem> oldMediaItems, List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
- attachRangeInfo(devices);
- Collections.sort(devices, Comparator.naturalOrder());
- }
// For the first time building list, to make sure the top device is the connected
// device.
boolean needToHandleMutingExpectedDevice =
@@ -648,8 +665,7 @@ public class MediaSwitchingController
.map(MediaItem::createDeviceMediaItem)
.collect(Collectors.toList());
- boolean shouldAddFirstSeenSelectedDevice =
- com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
+ boolean shouldAddFirstSeenSelectedDevice = Flags.enableOutputSwitcherDeviceGrouping();
if (shouldAddFirstSeenSelectedDevice) {
finalMediaItems.clear();
@@ -675,7 +691,7 @@ public class MediaSwitchingController
}
private boolean enableInputRouting() {
- return com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl();
+ return Flags.enableAudioInputDeviceRoutingAndVolumeControl();
}
private void buildInputMediaItems(List<MediaDevice> devices) {
@@ -703,8 +719,7 @@ public class MediaSwitchingController
if (connectedMediaDevice != null) {
selectedDevicesIds.add(connectedMediaDevice.getId());
}
- boolean groupSelectedDevices =
- com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
+ boolean groupSelectedDevices = Flags.enableOutputSwitcherDeviceGrouping();
int nextSelectedItemIndex = 0;
boolean suggestedDeviceAdded = false;
boolean displayGroupAdded = false;
@@ -879,6 +894,11 @@ public class MediaSwitchingController
return mLocalMediaManager.getCurrentConnectedDevice();
}
+ @VisibleForTesting
+ void clearMediaItemList() {
+ mOutputMediaItemListProxy.clear();
+ }
+
boolean addDeviceToPlayMedia(MediaDevice device) {
mMetricLogger.logInteractionExpansion(device);
return mLocalMediaManager.addDeviceToPlayMedia(device);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
index 1c9c0b102cb7..45ca2c6ee8e5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2020 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.
@@ -16,22 +16,175 @@
package com.android.systemui.media.dialog;
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+
+import com.android.media.flags.Flags;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.res.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
/** A proxy of holding the list of Output Switcher's output media items. */
public class OutputMediaItemListProxy {
+ private final Context mContext;
private final List<MediaItem> mOutputMediaItemList;
- public OutputMediaItemListProxy() {
+ // Use separated lists to hold different media items and create the list of output media items
+ // by using those separated lists and group dividers.
+ private final List<MediaItem> mSelectedMediaItems;
+ private final List<MediaItem> mSuggestedMediaItems;
+ private final List<MediaItem> mSpeakersAndDisplaysMediaItems;
+ @Nullable private MediaItem mConnectNewDeviceMediaItem;
+
+ public OutputMediaItemListProxy(Context context) {
+ mContext = context;
mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ mSelectedMediaItems = new CopyOnWriteArrayList<>();
+ mSuggestedMediaItems = new CopyOnWriteArrayList<>();
+ mSpeakersAndDisplaysMediaItems = new CopyOnWriteArrayList<>();
}
/** Returns the list of output media items. */
public List<MediaItem> getOutputMediaItemList() {
+ if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) {
+ if (isEmpty() && !mOutputMediaItemList.isEmpty()) {
+ // Ensures mOutputMediaItemList is empty when all individual media item lists are
+ // empty, preventing unexpected state issues.
+ mOutputMediaItemList.clear();
+ } else if (!isEmpty() && mOutputMediaItemList.isEmpty()) {
+ // When any individual media item list is modified, the cached mOutputMediaItemList
+ // is emptied. On the next request for the output media item list, a fresh list is
+ // created and stored in the cache.
+ mOutputMediaItemList.addAll(createOutputMediaItemList());
+ }
+ }
return mOutputMediaItemList;
}
+ private List<MediaItem> createOutputMediaItemList() {
+ List<MediaItem> finalMediaItems = new CopyOnWriteArrayList<>();
+ finalMediaItems.addAll(mSelectedMediaItems);
+ if (!mSuggestedMediaItems.isEmpty()) {
+ finalMediaItems.add(
+ MediaItem.createGroupDividerMediaItem(
+ mContext.getString(
+ R.string.media_output_group_title_suggested_device)));
+ finalMediaItems.addAll(mSuggestedMediaItems);
+ }
+ if (!mSpeakersAndDisplaysMediaItems.isEmpty()) {
+ finalMediaItems.add(
+ MediaItem.createGroupDividerMediaItem(
+ mContext.getString(
+ R.string.media_output_group_title_speakers_and_displays)));
+ finalMediaItems.addAll(mSpeakersAndDisplaysMediaItems);
+ }
+ if (mConnectNewDeviceMediaItem != null) {
+ finalMediaItems.add(mConnectNewDeviceMediaItem);
+ }
+ return finalMediaItems;
+ }
+
+ /** Updates the list of output media items with a given list of media devices. */
+ public void updateMediaDevices(
+ List<MediaDevice> devices,
+ List<MediaDevice> selectedDevices,
+ @Nullable MediaDevice connectedMediaDevice,
+ boolean needToHandleMutingExpectedDevice,
+ @Nullable MediaItem connectNewDeviceMediaItem) {
+ Set<String> selectedOrConnectedMediaDeviceIds =
+ selectedDevices.stream().map(MediaDevice::getId).collect(Collectors.toSet());
+ if (connectedMediaDevice != null) {
+ selectedOrConnectedMediaDeviceIds.add(connectedMediaDevice.getId());
+ }
+
+ List<MediaItem> selectedMediaItems = new ArrayList<>();
+ List<MediaItem> suggestedMediaItems = new ArrayList<>();
+ List<MediaItem> speakersAndDisplaysMediaItems = new ArrayList<>();
+ Map<String, MediaItem> deviceIdToMediaItemMap = new HashMap<>();
+ buildMediaItems(
+ devices,
+ selectedOrConnectedMediaDeviceIds,
+ needToHandleMutingExpectedDevice,
+ selectedMediaItems,
+ suggestedMediaItems,
+ speakersAndDisplaysMediaItems,
+ deviceIdToMediaItemMap);
+
+ List<MediaItem> updatedSelectedMediaItems = new CopyOnWriteArrayList<>();
+ List<MediaItem> updatedSuggestedMediaItems = new CopyOnWriteArrayList<>();
+ List<MediaItem> updatedSpeakersAndDisplaysMediaItems = new CopyOnWriteArrayList<>();
+ if (isEmpty()) {
+ updatedSelectedMediaItems.addAll(selectedMediaItems);
+ updatedSuggestedMediaItems.addAll(suggestedMediaItems);
+ updatedSpeakersAndDisplaysMediaItems.addAll(speakersAndDisplaysMediaItems);
+ } else {
+ Set<String> updatedDeviceIds = new HashSet<>();
+ // Preserve the existing media item order while updating with the latest device
+ // information. Some items may retain their original group (suggested, speakers and
+ // displays) to maintain this order.
+ updateMediaItems(
+ mSelectedMediaItems,
+ updatedSelectedMediaItems,
+ deviceIdToMediaItemMap,
+ updatedDeviceIds);
+ updateMediaItems(
+ mSuggestedMediaItems,
+ updatedSuggestedMediaItems,
+ deviceIdToMediaItemMap,
+ updatedDeviceIds);
+ updateMediaItems(
+ mSpeakersAndDisplaysMediaItems,
+ updatedSpeakersAndDisplaysMediaItems,
+ deviceIdToMediaItemMap,
+ updatedDeviceIds);
+
+ // Append new media items that are not already in the existing lists to the output list.
+ List<MediaItem> remainingMediaItems = new ArrayList<>();
+ remainingMediaItems.addAll(
+ getRemainingMediaItems(selectedMediaItems, updatedDeviceIds));
+ remainingMediaItems.addAll(
+ getRemainingMediaItems(suggestedMediaItems, updatedDeviceIds));
+ remainingMediaItems.addAll(
+ getRemainingMediaItems(speakersAndDisplaysMediaItems, updatedDeviceIds));
+ updatedSpeakersAndDisplaysMediaItems.addAll(remainingMediaItems);
+ }
+
+ if (Flags.enableOutputSwitcherDeviceGrouping() && !updatedSelectedMediaItems.isEmpty()) {
+ MediaItem selectedMediaItem = updatedSelectedMediaItems.get(0);
+ Optional<MediaDevice> mediaDeviceOptional = selectedMediaItem.getMediaDevice();
+ if (mediaDeviceOptional.isPresent()) {
+ MediaItem updatedMediaItem =
+ MediaItem.createDeviceMediaItem(
+ mediaDeviceOptional.get(), /* isFirstDeviceInGroup= */ true);
+ updatedSelectedMediaItems.remove(0);
+ updatedSelectedMediaItems.add(0, updatedMediaItem);
+ }
+ }
+
+ mSelectedMediaItems.clear();
+ mSelectedMediaItems.addAll(updatedSelectedMediaItems);
+ mSuggestedMediaItems.clear();
+ mSuggestedMediaItems.addAll(updatedSuggestedMediaItems);
+ mSpeakersAndDisplaysMediaItems.clear();
+ mSpeakersAndDisplaysMediaItems.addAll(updatedSpeakersAndDisplaysMediaItems);
+ mConnectNewDeviceMediaItem = connectNewDeviceMediaItem;
+
+ // The cached mOutputMediaItemList is cleared upon any update to individual media item
+ // lists. This ensures getOutputMediaItemList() computes and caches a fresh list on the next
+ // invocation.
+ mOutputMediaItemList.clear();
+ }
+
/** Updates the list of output media items with the given list. */
public void clearAndAddAll(List<MediaItem> updatedMediaItems) {
mOutputMediaItemList.clear();
@@ -40,16 +193,112 @@ public class OutputMediaItemListProxy {
/** Removes the media items with muting expected devices. */
public void removeMutingExpectedDevices() {
+ if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) {
+ mSelectedMediaItems.removeIf((MediaItem::isMutingExpectedDevice));
+ mSuggestedMediaItems.removeIf((MediaItem::isMutingExpectedDevice));
+ mSpeakersAndDisplaysMediaItems.removeIf((MediaItem::isMutingExpectedDevice));
+ if (mConnectNewDeviceMediaItem != null
+ && mConnectNewDeviceMediaItem.isMutingExpectedDevice()) {
+ mConnectNewDeviceMediaItem = null;
+ }
+ }
mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
}
/** Clears the output media item list. */
public void clear() {
+ if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) {
+ mSelectedMediaItems.clear();
+ mSuggestedMediaItems.clear();
+ mSpeakersAndDisplaysMediaItems.clear();
+ mConnectNewDeviceMediaItem = null;
+ }
mOutputMediaItemList.clear();
}
/** Returns whether the output media item list is empty. */
public boolean isEmpty() {
- return mOutputMediaItemList.isEmpty();
+ if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) {
+ return mSelectedMediaItems.isEmpty()
+ && mSuggestedMediaItems.isEmpty()
+ && mSpeakersAndDisplaysMediaItems.isEmpty()
+ && (mConnectNewDeviceMediaItem == null);
+ } else {
+ return mOutputMediaItemList.isEmpty();
+ }
+ }
+
+ private void buildMediaItems(
+ List<MediaDevice> devices,
+ Set<String> selectedOrConnectedMediaDeviceIds,
+ boolean needToHandleMutingExpectedDevice,
+ List<MediaItem> selectedMediaItems,
+ List<MediaItem> suggestedMediaItems,
+ List<MediaItem> speakersAndDisplaysMediaItems,
+ Map<String, MediaItem> deviceIdToMediaItemMap) {
+ for (MediaDevice device : devices) {
+ String deviceId = device.getId();
+ MediaItem mediaItem = MediaItem.createDeviceMediaItem(device);
+ if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) {
+ selectedMediaItems.add(0, mediaItem);
+ } else if (!needToHandleMutingExpectedDevice
+ && selectedOrConnectedMediaDeviceIds.contains(device.getId())) {
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
+ selectedMediaItems.add(mediaItem);
+ } else {
+ selectedMediaItems.add(0, mediaItem);
+ }
+ } else if (device.isSuggestedDevice()) {
+ suggestedMediaItems.add(mediaItem);
+ } else {
+ speakersAndDisplaysMediaItems.add(mediaItem);
+ }
+ deviceIdToMediaItemMap.put(deviceId, mediaItem);
+ }
+ }
+
+ /** Returns a list of media items that remains the same order as the existing media items. */
+ private void updateMediaItems(
+ List<MediaItem> existingMediaItems,
+ List<MediaItem> updatedMediaItems,
+ Map<String, MediaItem> deviceIdToMediaItemMap,
+ Set<String> updatedDeviceIds) {
+ List<String> existingDeviceIds = getDeviceIds(existingMediaItems);
+ for (String deviceId : existingDeviceIds) {
+ MediaItem mediaItem = deviceIdToMediaItemMap.get(deviceId);
+ if (mediaItem != null) {
+ updatedMediaItems.add(mediaItem);
+ updatedDeviceIds.add(deviceId);
+ }
+ }
+ }
+
+ /**
+ * Returns media items from the input list that are not associated with the given device IDs.
+ */
+ private List<MediaItem> getRemainingMediaItems(
+ List<MediaItem> mediaItems, Set<String> deviceIds) {
+ List<MediaItem> remainingMediaItems = new ArrayList<>();
+ for (MediaItem item : mediaItems) {
+ Optional<MediaDevice> mediaDeviceOptional = item.getMediaDevice();
+ if (mediaDeviceOptional.isPresent()) {
+ String deviceId = mediaDeviceOptional.get().getId();
+ if (!deviceIds.contains(deviceId)) {
+ remainingMediaItems.add(item);
+ }
+ }
+ }
+ return remainingMediaItems;
+ }
+
+ /** Returns a list of media device IDs for the given list of media items. */
+ private List<String> getDeviceIds(List<MediaItem> mediaItems) {
+ List<String> deviceIds = new ArrayList<>();
+ for (MediaItem item : mediaItems) {
+ if (item != null && item.getMediaDevice().isPresent()) {
+ deviceIds.add(item.getMediaDevice().get().getId());
+ }
+ }
+ return deviceIds;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt
index 4ff54d4eae65..42d27619f60f 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepository.kt
@@ -26,7 +26,7 @@ import android.os.IBinder
import android.util.Log
import android.view.Display
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
index 43bd6aa37b5a..faa77e51ec24 100644
--- a/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/data/repository/PowerRepository.kt
@@ -24,7 +24,7 @@ import android.content.IntentFilter
import android.os.PowerManager
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.power.shared.model.DozeScreenStateModel
diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
index 297c6af5a4a7..f368c53c5b39 100644
--- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakefulnessModel.kt
@@ -61,6 +61,11 @@ data class WakefulnessModel(
(lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
}
+ fun isAwakeFromMotionOrLift(): Boolean {
+ return isAwake() &&
+ (lastWakeReason == WakeSleepReason.MOTION || lastWakeReason == WakeSleepReason.LIFT)
+ }
+
override fun logDiffs(prevVal: WakefulnessModel, row: TableRowLogger) {
row.logChange(columnName = "wakefulness", value = toString())
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt
index bd9d70c13572..eb99fec7a0a8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/data/repository/ForegroundServicesRepository.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.footer.data.repository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.FgsManagerController
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt
index 1cd5d100ec00..e3a8ffd0f480 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/QSSettingsRestoredRepository.kt
@@ -6,7 +6,7 @@ import android.os.UserHandle
import android.provider.Settings
import android.util.Log
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.common.shared.model.PackageChangeModel.Empty.user
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt
index 88a49ee109aa..53d2554f0e82 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/CallbackControllerAutoAddable.kt
@@ -16,7 +16,7 @@
package com.android.systemui.qs.pipeline.domain.autoaddable
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
import com.android.systemui.qs.pipeline.domain.model.AutoAddable
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt
index 76bfad936116..a3b4c71c7641 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/DeviceControlsAutoAddable.kt
@@ -16,7 +16,7 @@
package com.android.systemui.qs.pipeline.domain.autoaddable
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
index e9c91ca0db12..66af6d8b3e18 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt
@@ -19,7 +19,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable
import android.content.Context
import android.hardware.display.ColorDisplayManager
import android.hardware.display.NightDisplayListener
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.NightDisplayListenerModule
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
index 88d7f06dfada..ff3fd3781181 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/SafetyCenterAutoAddable.kt
@@ -21,7 +21,7 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.text.TextUtils
import com.android.systemui.res.R
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt
index 3f619c08261d..c66c9dc9ba6a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/WorkTileAutoAddable.kt
@@ -18,7 +18,7 @@ package com.android.systemui.qs.pipeline.domain.autoaddable
import android.content.pm.UserInfo
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.pipeline.data.restoreprocessors.WorkTileRestoreProcessor
import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index 61a8fa3d2a6e..cd0b70e5e988 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -27,6 +27,7 @@ object SubtitleArrayMapping {
subtitleIdsMap["cell"] = R.array.tile_states_cell
subtitleIdsMap["battery"] = R.array.tile_states_battery
subtitleIdsMap["dnd"] = R.array.tile_states_dnd
+ subtitleIdsMap["modes_dnd"] = R.array.tile_states_modes_dnd
subtitleIdsMap["flashlight"] = R.array.tile_states_flashlight
subtitleIdsMap["rotation"] = R.array.tile_states_rotation
subtitleIdsMap["bt"] = R.array.tile_states_bt
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt
new file mode 100644
index 000000000000..52b02066c35a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesDndTile.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles
+
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.internal.logging.MetricsLogger
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.modes.shared.ModesUi
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.QSTile.BooleanState
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.qs.QSHost
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.asQSTileIcon
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.runBlocking
+
+/**
+ * Standalone tile used to control the DND Mode. Contrast to [ModesTile] (the tile that opens a
+ * dialog showing the list of all modes) and [DndTile] (the tile used to toggle interruption
+ * filtering in the pre-MODES_UI world).
+ */
+class ModesDndTile
+@Inject
+constructor(
+ host: QSHost,
+ uiEventLogger: QsEventLogger,
+ @Background backgroundLooper: Looper,
+ @Main mainHandler: Handler,
+ falsingManager: FalsingManager,
+ metricsLogger: MetricsLogger,
+ statusBarStateController: StatusBarStateController,
+ activityStarter: ActivityStarter,
+ qsLogger: QSLogger,
+ qsTileConfigProvider: QSTileConfigProvider,
+ private val dataInteractor: ModesDndTileDataInteractor,
+ private val tileMapper: ModesDndTileMapper,
+ private val userActionInteractor: ModesDndTileUserActionInteractor,
+) :
+ QSTileImpl<BooleanState>(
+ host,
+ uiEventLogger,
+ backgroundLooper,
+ mainHandler,
+ falsingManager,
+ metricsLogger,
+ statusBarStateController,
+ activityStarter,
+ qsLogger,
+ ) {
+
+ private lateinit var tileState: QSTileState
+ private val config = qsTileConfigProvider.getConfig(TILE_SPEC)
+
+ init {
+ lifecycle.coroutineScope.launch {
+ lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ dataInteractor.tileData().collect { refreshState(it) }
+ }
+ }
+ }
+
+ override fun isAvailable(): Boolean = ModesUi.isEnabled && android.app.Flags.modesUiDndTile()
+
+ override fun getTileLabel(): CharSequence =
+ mContext.getString(R.string.quick_settings_dnd_label)
+
+ override fun newTileState(): BooleanState = BooleanState()
+
+ override fun handleClick(expandable: Expandable?) = runBlocking {
+ userActionInteractor.handleClick()
+ }
+
+ override fun getLongClickIntent(): Intent? = userActionInteractor.getSettingsIntent()
+
+ @VisibleForTesting
+ public override fun handleUpdateState(state: BooleanState?, arg: Any?) {
+ val model = arg as? ModesDndTileModel ?: dataInteractor.getCurrentTileModel()
+
+ tileState = tileMapper.map(config, model)
+ state?.apply {
+ value = model.isActivated
+ this.state = tileState.activationState.legacyState
+ icon =
+ tileState.icon?.asQSTileIcon()
+ ?: maybeLoadResourceIcon(iconResId(model.isActivated))
+ label = tileLabel
+ secondaryLabel = tileState.secondaryLabel
+ contentDescription = tileState.contentDescription
+ expandedAccessibilityClassName = tileState.expandedAccessibilityClassName
+ }
+ }
+
+ @DrawableRes
+ private fun iconResId(activated: Boolean): Int =
+ if (activated) R.drawable.qs_dnd_icon_on else R.drawable.qs_dnd_icon_off
+
+ companion object {
+ const val TILE_SPEC = "modes_dnd"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
index 1544804c3291..38eb5947bd71 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.tiles.impl.flashlight.domain.interactor
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt
new file mode 100644
index 000000000000..b1ae3ba4381a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileDataInteractor.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import android.content.Context
+import android.os.UserHandle
+import com.android.app.tracing.coroutines.flow.flowName
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.modes.shared.ModesUi
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+class ModesDndTileDataInteractor
+@Inject
+constructor(
+ @ShadeDisplayAware val context: Context,
+ val zenModeInteractor: ZenModeInteractor,
+ @Background val bgDispatcher: CoroutineDispatcher,
+) : QSTileDataInteractor<ModesDndTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>,
+ ): Flow<ModesDndTileModel> = tileData()
+
+ /**
+ * An adapted version of the base class' [tileData] method for use in an old-style tile.
+ *
+ * TODO(b/299909989): Remove after the transition.
+ */
+ fun tileData() =
+ zenModeInteractor.dndMode
+ .filterNotNull()
+ .map { dndMode -> buildTileData(dndMode) }
+ .flowName("tileData")
+ .flowOn(bgDispatcher)
+ .distinctUntilChanged()
+
+ fun getCurrentTileModel() = buildTileData(zenModeInteractor.getDndMode())
+
+ private fun buildTileData(dndMode: ZenMode): ModesDndTileModel {
+ return ModesDndTileModel(isActivated = dndMode.isActive)
+ }
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(ModesUi.isEnabled && android.app.Flags.modesUiDndTile())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt
new file mode 100644
index 000000000000..e8fcea070ede
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesDndTileUserActionInteractor.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
+import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID
+import android.util.Log
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogEventLogger
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class ModesDndTileUserActionInteractor
+@Inject
+constructor(
+ @Main private val mainContext: CoroutineContext,
+ private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler,
+ // TODO(b/353896370): The domain layer should not have to depend on the UI layer.
+ private val dialogDelegate: ModesDialogDelegate,
+ private val zenModeInteractor: ZenModeInteractor,
+ private val dialogEventLogger: ModesDialogEventLogger,
+ private val settingsPackageRepository: QSSettingsPackageRepository,
+) : QSTileUserActionInteractor<ModesDndTileModel> {
+
+ override suspend fun handleInput(input: QSTileInput<ModesDndTileModel>) {
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click,
+ is QSTileUserAction.ToggleClick -> {
+ handleClick()
+ }
+ is QSTileUserAction.LongClick -> {
+ handleLongClick(action.expandable)
+ }
+ }
+ }
+ }
+
+ suspend fun handleClick() {
+ val dnd = zenModeInteractor.dndMode.value
+ if (dnd == null) {
+ Log.wtf(TAG, "No DND!?")
+ return
+ }
+
+ if (!dnd.isActive) {
+ if (zenModeInteractor.shouldAskForZenDuration(dnd)) {
+ dialogEventLogger.logOpenDurationDialog(dnd)
+ withContext(mainContext) {
+ // NOTE: The dialog handles turning on the mode itself.
+ val dialog = dialogDelegate.makeDndDurationDialog()
+ dialog.show()
+ }
+ } else {
+ dialogEventLogger.logModeOn(dnd)
+ zenModeInteractor.activateMode(dnd)
+ }
+ } else {
+ dialogEventLogger.logModeOff(dnd)
+ zenModeInteractor.deactivateMode(dnd)
+ }
+ }
+
+ private fun handleLongClick(expandable: Expandable?) {
+ val intent = getSettingsIntent()
+ if (intent != null) {
+ qsTileIntentUserInputHandler.handle(expandable, intent)
+ }
+ }
+
+ fun getSettingsIntent(): Intent? {
+ val dnd = zenModeInteractor.dndMode.value
+ if (dnd == null) {
+ Log.wtf(TAG, "No DND!?")
+ return null
+ }
+
+ return Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+ .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, dnd.id)
+ .setPackage(settingsPackageRepository.getSettingsPackageName())
+ }
+
+ companion object {
+ const val TAG = "ModesDndTileUserActionInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt
new file mode 100644
index 000000000000..eab798897aa3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesDndTileModel.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.domain.model
+
+data class ModesDndTileModel(val isActivated: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt
new file mode 100644
index 000000000000..4869b6f74554
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesDndTileMapper.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.modes.ui
+
+import android.content.res.Resources
+import android.widget.Switch
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.shade.ShadeDisplayAware
+import javax.inject.Inject
+
+class ModesDndTileMapper
+@Inject
+constructor(@ShadeDisplayAware private val resources: Resources, val theme: Resources.Theme) :
+ QSTileDataToStateMapper<ModesDndTileModel> {
+ override fun map(config: QSTileConfig, data: ModesDndTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ val iconResource =
+ if (data.isActivated) R.drawable.qs_dnd_icon_on else R.drawable.qs_dnd_icon_off
+ icon =
+ Icon.Loaded(
+ resources.getDrawable(iconResource, theme),
+ res = iconResource,
+ contentDescription = null,
+ )
+
+ activationState =
+ if (data.isActivated) {
+ QSTileState.ActivationState.ACTIVE
+ } else {
+ QSTileState.ActivationState.INACTIVE
+ }
+ label = resources.getString(R.string.quick_settings_dnd_label)
+ secondaryLabel =
+ resources.getString(
+ if (data.isActivated) R.string.zen_mode_on else R.string.zen_mode_off
+ )
+ contentDescription = label
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ expandedAccessibilityClass = Switch::class
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt
index 7117629622e6..a8e9c5663f39 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/SensorPrivacyToggleTileDataInteractor.kt
@@ -22,7 +22,7 @@ import android.hardware.SensorPrivacyManager.Sensors.Sensor
import android.os.UserHandle
import android.provider.DeviceConfig
import android.util.Log
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 7bb831baec20..3ad0867192d3 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -33,7 +33,7 @@ import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.classifier.FalsingCollectorActual
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.DisplayId
diff --git a/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt
index 7e967f436ecb..0b039fecc19e 100644
--- a/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/security/data/repository/SecurityRepository.kt
@@ -17,7 +17,7 @@
package com.android.systemui.security.data.repository
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.security.data.model.SecurityModel
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt
index 91c92cc8cd2f..ce74cb7d54d2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt
@@ -20,7 +20,7 @@ import android.content.IntentFilter
import android.os.UserHandle
import android.safetycenter.SafetyCenterManager
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 657c86b10f16..e66b21866902 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1088,14 +1088,18 @@ public class KeyguardIndicationController {
if (!TextUtils.equals(mTopIndicationView.getText(), newIndication)) {
mWakeLock.setAcquired(true);
+ final KeyguardIndication.Builder builder = new KeyguardIndication.Builder()
+ .setMessage(newIndication)
+ .setTextColor(ColorStateList.valueOf(
+ useMisalignmentColor
+ ? mContext.getColor(R.color.misalignment_text_color)
+ : Color.WHITE));
+ if (mBiometricMessage != null && newIndication == mBiometricMessage) {
+ builder.setForceAccessibilityLiveRegionAssertive();
+ }
+
mTopIndicationView.switchIndication(newIndication,
- new KeyguardIndication.Builder()
- .setMessage(newIndication)
- .setTextColor(ColorStateList.valueOf(
- useMisalignmentColor
- ? mContext.getColor(R.color.misalignment_text_color)
- : Color.WHITE))
- .build(),
+ builder.build(),
true, () -> mWakeLock.setAcquired(false));
}
return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index 3180a06ae787..fdcd39d1d616 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -34,7 +34,7 @@ public interface NotificationLockscreenUserManager {
value = {
REDACTION_TYPE_NONE,
REDACTION_TYPE_PUBLIC,
- REDACTION_TYPE_SENSITIVE_CONTENT})
+ REDACTION_TYPE_OTP})
@interface RedactionType {}
/**
@@ -52,7 +52,7 @@ public interface NotificationLockscreenUserManager {
* Indicates that a notification should have its main content redacted, due to detected
* sensitive content, such as a One-Time Password
*/
- int REDACTION_TYPE_SENSITIVE_CONTENT = 1 << 1;
+ int REDACTION_TYPE_OTP = 1 << 1;
/**
* @param userId user Id
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index ec04edb0f810..339f898be251 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -795,7 +795,7 @@ public class NotificationLockscreenUserManagerImpl implements
}
if (shouldShowSensitiveContentRedactedView(ent)) {
- return REDACTION_TYPE_SENSITIVE_CONTENT;
+ return REDACTION_TYPE_OTP;
}
return REDACTION_TYPE_NONE;
}
@@ -920,7 +920,7 @@ public class NotificationLockscreenUserManagerImpl implements
// notification's "when" time, or the notification entry creation time
private long getEarliestNotificationTime(NotificationEntry notif) {
long notifWhenWallClock = notif.getSbn().getNotification().getWhen();
- long creationTimeDelta = SystemClock.elapsedRealtime() - notif.getCreationTime();
+ long creationTimeDelta = SystemClock.uptimeMillis() - notif.getCreationTime();
long creationTimeWallClock = System.currentTimeMillis() - creationTimeDelta;
return Math.min(notifWhenWallClock, creationTimeWallClock);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt
index 6148b407d3bf..8fc95092be10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.plugins.statusbar.StatusBarStateController
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
index aeeb0427d24b..e91e9777d48e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
@@ -14,7 +14,7 @@
package com.android.systemui.statusbar.disableflags.data.repository
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.DisplayId
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 3d8ba7f335bd..d031d831bf5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -29,8 +29,6 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICAT
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
-import static com.android.systemui.statusbar.notification.collection.BundleEntry.ROOT_BUNDLES;
-import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY;
import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED;
import static java.util.Objects.requireNonNull;
@@ -41,7 +39,6 @@ import android.app.Notification;
import android.app.Notification.MessagingStyle.Message;
import android.app.NotificationChannel;
import android.app.NotificationManager.Policy;
-import android.app.PendingIntent;
import android.app.Person;
import android.app.RemoteInput;
import android.app.RemoteInputHistoryItem;
@@ -68,7 +65,6 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.icon.IconPack;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
@@ -81,14 +77,14 @@ import com.android.systemui.statusbar.notification.row.shared.NotificationRowCon
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.util.ListenerSet;
-import kotlinx.coroutines.flow.MutableStateFlow;
-import kotlinx.coroutines.flow.StateFlow;
-import kotlinx.coroutines.flow.StateFlowKt;
-
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
/**
* Represents a notification that the system UI knows about
*
@@ -497,7 +493,8 @@ public final class NotificationEntry extends ListEntry {
public @Nullable List<NotificationEntry> getAttachedNotifChildren() {
if (NotificationBundleUi.isEnabled()) {
if (isGroupSummary()) {
- return ((GroupEntry) getParent()).getChildren();
+ GroupEntry parent = (GroupEntry) getParent();
+ return parent != null ? new ArrayList<>(parent.getChildren()) : null;
}
} else {
if (row == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index 1f32b945ce7e..cda535de86c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.collection.coordinator;
+import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -99,7 +101,11 @@ public class RankingCoordinator implements Coordinator {
NotificationPriorityBucketKt.BUCKET_ALERTING) {
@Override
public boolean isInSection(PipelineEntry entry) {
- return mHighPriorityProvider.isHighPriority(entry);
+ return mHighPriorityProvider.isHighPriority(entry)
+ && entry.getRepresentativeEntry() != null
+ && entry.getRepresentativeEntry().getChannel() != null
+ && !SYSTEM_RESERVED_IDS.contains(
+ entry.getRepresentativeEntry().getChannel().getId());
}
@Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt
index 6525b6f1186b..376735025abd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerExt.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.headsup
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
index d02f636728fc..fe3a856e711e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt
@@ -18,7 +18,9 @@ package com.android.systemui.statusbar.notification.row
import android.animation.ValueAnimator
import android.content.Context
+import android.content.res.ColorStateList
import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.Path
@@ -31,9 +33,27 @@ import android.graphics.RectF
import android.graphics.Shader
import com.android.systemui.res.R
import com.android.wm.shell.shared.animation.Interpolators
+import android.graphics.drawable.RippleDrawable
+import androidx.core.content.ContextCompat
class MagicActionBackgroundDrawable(
context: Context,
+) : RippleDrawable(
+ ContextCompat.getColorStateList(
+ context,
+ R.color.notification_ripple_untinted_color
+ ) ?: ColorStateList.valueOf(Color.TRANSPARENT),
+ createBaseDrawable(context), null
+) {
+ companion object {
+ private fun createBaseDrawable(context: Context): Drawable {
+ return BaseBackgroundDrawable(context)
+ }
+ }
+}
+
+class BaseBackgroundDrawable(
+ context: Context,
) : Drawable() {
private val cornerRadius = context.resources.getDimension(R.dimen.magic_action_button_corner_radius)
@@ -42,6 +62,14 @@ class MagicActionBackgroundDrawable(
private val buttonShape = Path()
// Color and style
+ private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ val bgColor =
+ context.getColor(
+ com.android.internal.R.color.materialColorSurfaceContainerHigh
+ )
+ color = bgColor
+ style = Paint.Style.FILL
+ }
private val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
val outlineColor =
context.getColor(
@@ -91,6 +119,7 @@ class MagicActionBackgroundDrawable(
canvas.save()
// Draw background
canvas.clipPath(buttonShape)
+ canvas.drawPath(buttonShape, bgPaint)
// Apply gradient to outline
canvas.drawPath(buttonShape, outlinePaint)
updateGradient(boundsF)
@@ -119,11 +148,13 @@ class MagicActionBackgroundDrawable(
}
override fun setAlpha(alpha: Int) {
+ bgPaint.alpha = alpha
outlinePaint.alpha = alpha
invalidateSelf()
}
override fun setColorFilter(colorFilter: ColorFilter?) {
+ bgPaint.colorFilter = colorFilter
outlinePaint.colorFilter = colorFilter
invalidateSelf()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 3ffc052b5acc..ff4b835eb3c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.notification.row;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
+import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
@@ -236,7 +236,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
);
}
if (LockscreenOtpRedaction.isSingleLineViewEnabled()) {
- if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ if (bindParams.redactionType == REDACTION_TYPE_OTP) {
result.mPublicInflatedSingleLineViewModel =
SingleLineViewInflater.inflateSingleLineViewModel(
entry.getSbn().getNotification(),
@@ -469,7 +469,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
logger.logAsyncTaskProgress(row.getLoggingKey(), "creating public remote view");
if (LockscreenOtpRedaction.isEnabled()
- && bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ && bindParams.redactionType == REDACTION_TYPE_OTP) {
result.newPublicView = createSensitiveContentMessageNotification(
NotificationBundleUi.isEnabled()
? row.getEntryAdapter().getSbn().getNotification()
@@ -1355,7 +1355,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder
}
if (LockscreenOtpRedaction.isSingleLineViewEnabled()) {
- if (mBindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ if (mBindParams.redactionType == REDACTION_TYPE_OTP) {
result.mPublicInflatedSingleLineViewModel =
SingleLineViewInflater.inflateSingleLineViewModel(
mEntry.getSbn().getNotification(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index cd6e4979ce88..b3357d01ab7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1247,7 +1247,7 @@ public class NotificationContentView extends FrameLayout implements Notification
final boolean isSingleLineViewPresent = mSingleLineView != null;
if (shouldShowSingleLineView && !isSingleLineViewPresent) {
- Log.wtf(TAG, "calculateVisibleType: SingleLineView is not available!");
+ Log.e(TAG, "calculateVisibleType: SingleLineView is not available!");
}
final int collapsedVisualType = shouldShowSingleLineView && isSingleLineViewPresent
@@ -1274,7 +1274,7 @@ public class NotificationContentView extends FrameLayout implements Notification
final boolean shouldShowSingleLineView = mIsChildInGroup && !isGroupExpanded();
final boolean isSingleLinePresent = mSingleLineView != null;
if (shouldShowSingleLineView && !isSingleLinePresent) {
- Log.wtf(TAG, "getVisualTypeForHeight: singleLineView is not available.");
+ Log.e(TAG, "getVisualTypeForHeight: singleLineView is not available.");
}
if (!mUserExpanding && shouldShowSingleLineView && isSingleLinePresent) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index b1c145e08777..2f94d3220dc8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.notification.row
import android.annotation.SuppressLint
-import android.app.Flags
import android.app.Notification
import android.app.Notification.EXTRA_SUMMARIZED_CONTENT
import android.app.Notification.MessagingStyle
@@ -45,7 +44,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.NotifInflation
import com.android.systemui.res.R
import com.android.systemui.statusbar.InflationTask
-import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT
+import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_OTP
import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.InflationException
@@ -758,7 +757,7 @@ constructor(
entry.logKey,
"inflating public single line view model",
)
- if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ if (bindParams.redactionType == REDACTION_TYPE_OTP) {
SingleLineViewInflater.inflateSingleLineViewModel(
notification = entry.sbn.notification,
messagingStyle = messagingStyle,
@@ -876,7 +875,7 @@ constructor(
logger.logAsyncTaskProgress(row.loggingKey, "creating public remote view")
if (
LockscreenOtpRedaction.isEnabled &&
- bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT
+ bindParams.redactionType == REDACTION_TYPE_OTP
) {
createSensitiveContentMessageNotification(
entry.sbn.notification,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 1e249520e8b3..abfb86244390 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -36,13 +36,14 @@ import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository;
+import com.android.systemui.statusbar.notification.headsup.AvalancheController;
+import com.android.systemui.statusbar.notification.headsup.NotificationsHunSharedAnimationValues;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.BypassController;
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.notification.headsup.AvalancheController;
import java.io.PrintWriter;
@@ -424,6 +425,7 @@ public class AmbientState implements Dumpable {
/** the bottom-most y position where we can draw pinned HUNs */
public float getHeadsUpBottom() {
if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ NotificationsHunSharedAnimationValues.assertInLegacyMode();
return mHeadsUpBottom;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index d23a4c6307fc..28218227506c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -967,9 +967,12 @@ public class StackScrollAlgorithm {
childState.setZTranslation(baseZ);
}
if (isTopEntry && row.isAboveShelf()) {
+ float headsUpBottom = NotificationsHunSharedAnimationValues.isEnabled()
+ ? mHeadsUpAnimator.getHeadsUpAppearHeightBottom()
+ : ambientState.getHeadsUpBottom();
clampHunToMaxTranslation(
/* headsUpTop = */ headsUpTranslation,
- /* headsUpBottom = */ ambientState.getHeadsUpBottom(),
+ /* headsUpBottom = */ headsUpBottom,
/* viewState = */ childState
);
updateCornerRoundnessForPinnedHun(row, ambientState.getStackTop());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt
index fbc6b9524a6d..372e91f88ae5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.phone
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
index f82e681de76f..e2fd4bdc45a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt
@@ -19,7 +19,7 @@ package com.android.systemui.statusbar.pipeline.airplane.data.repository
import android.net.ConnectivityManager
import android.os.Handler
import android.provider.Settings.Global
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt
index 732ea6ac6790..5127e8a14796 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt
@@ -101,7 +101,13 @@ fun BatteryCanvas(
for (glyph in glyphs) {
// Move the glyph to the right spot
val verticalOffset = (BatteryFrame.innerHeight - glyph.height) / 2
- inset(horizontalOffset, verticalOffset) { glyph.draw(this, colors) }
+ inset(
+ // Never try and inset more than half of the available size - see b/400246091.
+ minOf(horizontalOffset, size.width / 2),
+ minOf(verticalOffset, size.height / 2),
+ ) {
+ glyph.draw(this, colors)
+ }
horizontalOffset += glyph.width + INTER_GLYPH_PADDING_PX
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt
index 1f5b849c56cc..f4076ae34a4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt
@@ -24,7 +24,7 @@ import com.android.systemui.Flags
import com.android.systemui.KairosActivatable
import com.android.systemui.KairosBuilder
import com.android.systemui.activated
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 982f6ec36150..2a6ff3b98ae2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -28,7 +28,7 @@ import android.telephony.satellite.SatelliteModemStateCallback
import android.telephony.satellite.SatelliteProvisionStateCallback
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index f9bba9d624f1..eaceb5e22535 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -26,7 +26,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Flags.multiuserWifiPickerTrackerSupport
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
index 0a2bbe580b99..14cadd90db4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
@@ -14,7 +14,7 @@
package com.android.systemui.statusbar.policy
import android.content.res.Configuration
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 3cb7090ea6d4..a352982f58f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -32,6 +32,7 @@ import com.android.systemui.qs.tiles.DndTile
import com.android.systemui.qs.tiles.FlashlightTile
import com.android.systemui.qs.tiles.LocationTile
import com.android.systemui.qs.tiles.MicrophoneToggleTile
+import com.android.systemui.qs.tiles.ModesDndTile
import com.android.systemui.qs.tiles.ModesTile
import com.android.systemui.qs.tiles.UiModeNightTile
import com.android.systemui.qs.tiles.WorkModeTile
@@ -49,9 +50,13 @@ import com.android.systemui.qs.tiles.impl.location.domain.LocationTileMapper
import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileDataInteractor
import com.android.systemui.qs.tiles.impl.location.domain.interactor.LocationTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileDataInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesDndTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesDndTileModel
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
+import com.android.systemui.qs.tiles.impl.modes.ui.ModesDndTileMapper
import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
import com.android.systemui.qs.tiles.impl.sensorprivacy.SensorPrivacyToggleTileDataInteractor
import com.android.systemui.qs.tiles.impl.sensorprivacy.domain.SensorPrivacyToggleTileUserActionInteractor
@@ -132,6 +137,7 @@ interface PolicyModule {
const val CAMERA_TOGGLE_TILE_SPEC = "cameratoggle"
const val MIC_TOGGLE_TILE_SPEC = "mictoggle"
const val DND_TILE_SPEC = "dnd"
+ const val MODES_DND_TILE_SPEC = "modes_dnd"
/** Inject DndTile or ModesTile into tileMap in QSModule based on feature flag */
@Provides
@@ -146,6 +152,12 @@ interface PolicyModule {
return if (ModesUi.isEnabled) modesTile.get() else dndTile.get()
}
+ /** Inject ModesDndTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(MODES_DND_TILE_SPEC)
+ fun bindDndModeTile(tile: ModesDndTile): QSTileImpl<*> = tile
+
/** Inject flashlight config */
@Provides
@IntoMap
@@ -449,6 +461,37 @@ interface PolicyModule {
mapper,
)
else StubQSTileViewModel
+
+ @Provides
+ @IntoMap
+ @StringKey(MODES_DND_TILE_SPEC)
+ fun provideDndModeTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(MODES_DND_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_dnd_icon_off,
+ labelRes = R.string.quick_settings_dnd_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ category = TileCategory.CONNECTIVITY,
+ )
+
+ @Provides
+ @IntoMap
+ @StringKey(MODES_DND_TILE_SPEC)
+ fun provideDndModeTileViewModel(
+ factory: QSTileViewModelFactory.Static<ModesDndTileModel>,
+ mapper: ModesDndTileMapper,
+ stateInteractor: ModesDndTileDataInteractor,
+ userActionInteractor: ModesDndTileUserActionInteractor,
+ ): QSTileViewModel =
+ factory.create(
+ TileSpec.create(MODES_DND_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
}
/** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
index 07bbca74e12e..2b9d39a54c44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
@@ -15,7 +15,7 @@
*/
package com.android.systemui.statusbar.policy.data.repository
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import dagger.Binds
import dagger.Module
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 e8347df5653f..ed814c6b3785 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
@@ -58,7 +58,7 @@ import kotlinx.coroutines.flow.stateIn
* An interactor that performs business logic related to the status and configuration of Zen Mode
* (or Do Not Disturb/DND Mode).
*/
- @SysUISingleton
+@SysUISingleton
class ZenModeInteractor
@Inject
constructor(
@@ -141,6 +141,18 @@ constructor(
return field
}
+ /**
+ * Returns the current state of the special "manual DND" mode.
+ *
+ * This should only be used when there is a strong reason to handle DND specifically (such as
+ * legacy UI pieces that haven't been updated to use modes more generally, or if the user
+ * explicitly wants a shortcut to DND). Please prefer using [modes] or [activeModes] in all
+ * other scenarios.
+ */
+ fun getDndMode(): ZenMode {
+ return zenModeRepository.getModes().single { it.isManualDnd }
+ }
+
/** Flow returning the currently active mode(s), if any. */
val activeModes: Flow<ActiveZenModes> =
modes
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
index 2dc17f40a380..c86e00de4246 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.ui.viewmodel
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
index b1b6014bfbde..9b88d439f2db 100644
--- a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
@@ -23,7 +23,7 @@ import android.content.pm.PackageManager
import android.telecom.TelecomManager
import android.telephony.Annotation
import android.telephony.TelephonyCallback
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
index fbbd2b9c5de8..e47d74ec9412 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
@@ -16,7 +16,7 @@
package com.android.systemui.unfold.data.repository
import androidx.annotation.FloatRange
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionInProgress
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index c960b5525d96..05b2e0d1423e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -33,7 +33,7 @@ import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.statusbar.IStatusBarService
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
index bcbd679b35eb..412161cf98bc 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserSwitcherRepository.kt
@@ -23,7 +23,7 @@ import android.os.UserManager
import android.provider.Settings.Global.USER_SWITCHER_ENABLED
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
index 31a8d864de95..9937eeb29151 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
@@ -19,7 +19,7 @@ import android.content.ContentResolver
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
index 80ccd646f6be..d4eabb9264e6 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BatteryControllerExt.kt
@@ -16,7 +16,7 @@
package com.android.systemui.util.kotlin
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.statusbar.policy.BatteryController
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
index 7a2f9b24700f..837bbea9cc3c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt
@@ -16,7 +16,7 @@
package com.android.systemui.util.kotlin
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.statusbar.phone.ManagedProfileController
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
index ee00e8b04ef1..02012ede697b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ReduceBrightColorsControllerExt.kt
@@ -16,7 +16,7 @@
package com.android.systemui.util.kotlin
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.qs.ReduceBrightColorsController
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt
index 22cc8dd7745d..a914c86da0e7 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/RotationLockControllerExt.kt
@@ -16,7 +16,7 @@
package com.android.systemui.util.kotlin
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.statusbar.policy.RotationLockController
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
index 594c5526dca9..7e9ebd218787 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -24,7 +24,7 @@ import android.service.quickaccesswallet.QuickAccessWalletClient
import android.service.quickaccesswallet.WalletCard
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
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 5c26dac5eb30..798aa428e73e 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
@@ -60,13 +60,13 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.service.notification.StatusBarNotification;
import android.testing.TestableLooper;
import android.text.TextUtils;
import android.view.View;
import androidx.core.graphics.drawable.IconCompat;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.media.flags.Flags;
@@ -101,6 +101,9 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -108,7 +111,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class MediaSwitchingControllerTest extends SysuiTestCase {
private static final String TEST_DEVICE_1_ID = "test_device_1_id";
@@ -201,6 +204,17 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
private MediaDescription mMediaDescription;
private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION,
+ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING);
+ }
+
+ public MediaSwitchingControllerTest(FlagsParameterization flags) {
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
public void setUp() {
mPackageName = mContext.getPackageName();
@@ -260,7 +274,6 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
mMediaDevices.add(mMediaDevice1);
mMediaDevices.add(mMediaDevice2);
-
when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID);
when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR);
when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID);
@@ -689,7 +702,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.clearMediaItemList();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
final List<MediaDevice> devices = new ArrayList<>();
int dividerSize = 0;
@@ -1528,7 +1541,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.clearMediaItemList();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
@@ -1546,7 +1559,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.clearMediaItemList();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
@@ -1564,7 +1577,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.clearMediaItemList();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
@@ -1582,7 +1595,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
.getSelectedMediaDevice();
mMediaSwitchingController.start(mCb);
reset(mCb);
- mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.clearMediaItemList();
mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
mMediaDevices.clear();
mMediaDevices.add(mMediaDevice2);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java
new file mode 100644
index 000000000000..f6edd49f142f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/OutputMediaItemListProxyTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.media.flags.Flags;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4.class)
+@TestableLooper.RunWithLooper
+public class OutputMediaItemListProxyTest extends SysuiTestCase {
+ private static final String DEVICE_ID_1 = "device_id_1";
+ private static final String DEVICE_ID_2 = "device_id_2";
+ private static final String DEVICE_ID_3 = "device_id_3";
+ private static final String DEVICE_ID_4 = "device_id_4";
+ @Mock private MediaDevice mMediaDevice1;
+ @Mock private MediaDevice mMediaDevice2;
+ @Mock private MediaDevice mMediaDevice3;
+ @Mock private MediaDevice mMediaDevice4;
+
+ private MediaItem mMediaItem1;
+ private MediaItem mMediaItem2;
+ private MediaItem mConnectNewDeviceMediaItem;
+ private OutputMediaItemListProxy mOutputMediaItemListProxy;
+
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION,
+ Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING);
+ }
+
+ public OutputMediaItemListProxyTest(FlagsParameterization flags) {
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mMediaDevice1.getId()).thenReturn(DEVICE_ID_1);
+ when(mMediaDevice2.getId()).thenReturn(DEVICE_ID_2);
+ when(mMediaDevice2.isSuggestedDevice()).thenReturn(true);
+ when(mMediaDevice3.getId()).thenReturn(DEVICE_ID_3);
+ when(mMediaDevice4.getId()).thenReturn(DEVICE_ID_4);
+ mMediaItem1 = MediaItem.createDeviceMediaItem(mMediaDevice1);
+ mMediaItem2 = MediaItem.createDeviceMediaItem(mMediaDevice2);
+ mConnectNewDeviceMediaItem = MediaItem.createPairNewDeviceMediaItem();
+
+ mOutputMediaItemListProxy = new OutputMediaItemListProxy(mContext);
+ }
+
+ @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void updateMediaDevices_shouldUpdateMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ // Create the initial output media item list with mMediaDevice2 and mMediaDevice3.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice2, mMediaDevice3),
+ /* selectedDevices */ List.of(mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ // Check the output media items to be
+ // * a media item with the selected mMediaDevice3
+ // * a group divider for suggested devices
+ // * a media item with the mMediaDevice2
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(mMediaDevice3, null, mMediaDevice2);
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList().get(0).isFirstDeviceInGroup())
+ .isEqualTo(Flags.enableOutputSwitcherDeviceGrouping());
+
+ // Update the output media item list with more media devices.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2),
+ /* selectedDevices */ List.of(mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ // Check the output media items to be
+ // * a media item with the selected route mMediaDevice3
+ // * a group divider for suggested devices
+ // * a media item with the route mMediaDevice2
+ // * a group divider for speakers and displays
+ // * a media item with the route mMediaDevice4
+ // * a media item with the route mMediaDevice1
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(
+ mMediaDevice3, null, mMediaDevice2, null, mMediaDevice4, mMediaDevice1);
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList().get(0).isFirstDeviceInGroup())
+ .isEqualTo(Flags.enableOutputSwitcherDeviceGrouping());
+
+ // Update the output media item list where mMediaDevice4 is offline and new selected device.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice2),
+ /* selectedDevices */ List.of(mMediaDevice1, mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ // Check the output media items to be
+ // * a media item with the selected route mMediaDevice3
+ // * a group divider for suggested devices
+ // * a media item with the route mMediaDevice2
+ // * a group divider for speakers and displays
+ // * a media item with the route mMediaDevice1
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(mMediaDevice3, null, mMediaDevice2, null, mMediaDevice1);
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList().get(0).isFirstDeviceInGroup())
+ .isEqualTo(Flags.enableOutputSwitcherDeviceGrouping());
+ }
+
+ @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void updateMediaDevices_multipleSelectedDevices_shouldHaveCorrectDeviceOrdering() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ // Create the initial output media item list with mMediaDevice2 and mMediaDevice3.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice2, mMediaDevice4, mMediaDevice3, mMediaDevice1),
+ /* selectedDevices */ List.of(mMediaDevice1, mMediaDevice2, mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
+ // When the device grouping is enabled, the order of selected devices are preserved:
+ // * a media item with the selected mMediaDevice2
+ // * a media item with the selected mMediaDevice3
+ // * a media item with the selected mMediaDevice1
+ // * a group divider for speakers and displays
+ // * a media item with the mMediaDevice4
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(
+ mMediaDevice2, mMediaDevice3, mMediaDevice1, null, mMediaDevice4);
+ assertThat(
+ mOutputMediaItemListProxy
+ .getOutputMediaItemList()
+ .get(0)
+ .isFirstDeviceInGroup())
+ .isTrue();
+ } else {
+ // When the device grouping is disabled, the order of selected devices are reverted:
+ // * a media item with the selected mMediaDevice1
+ // * a media item with the selected mMediaDevice3
+ // * a media item with the selected mMediaDevice2
+ // * a group divider for speakers and displays
+ // * a media item with the mMediaDevice4
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(
+ mMediaDevice1, mMediaDevice3, mMediaDevice2, null, mMediaDevice4);
+ }
+
+ // Update the output media item list with a selected device being deselected.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice4, mMediaDevice1, mMediaDevice3, mMediaDevice2),
+ /* selectedDevices */ List.of(mMediaDevice2, mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
+ // When the device grouping is enabled, the order of selected devices are preserved:
+ // * a media item with the selected mMediaDevice2
+ // * a media item with the selected mMediaDevice3
+ // * a media item with the selected mMediaDevice1
+ // * a group divider for speakers and displays
+ // * a media item with the mMediaDevice4
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(
+ mMediaDevice2, mMediaDevice3, mMediaDevice1, null, mMediaDevice4);
+ assertThat(
+ mOutputMediaItemListProxy
+ .getOutputMediaItemList()
+ .get(0)
+ .isFirstDeviceInGroup())
+ .isTrue();
+ } else {
+ // When the device grouping is disabled, the order of selected devices are reverted:
+ // * a media item with the selected mMediaDevice1
+ // * a media item with the selected mMediaDevice3
+ // * a media item with the selected mMediaDevice2
+ // * a group divider for speakers and displays
+ // * a media item with the mMediaDevice4
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(
+ mMediaDevice1, mMediaDevice3, mMediaDevice2, null, mMediaDevice4);
+ }
+
+ // Update the output media item list with a selected device is missing.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice1, mMediaDevice3, mMediaDevice4),
+ /* selectedDevices */ List.of(mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
+ // When the device grouping is enabled, the order of selected devices are preserved:
+ // * a media item with the selected mMediaDevice3
+ // * a media item with the selected mMediaDevice1
+ // * a group divider for speakers and displays
+ // * a media item with the mMediaDevice4
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(mMediaDevice3, mMediaDevice1, null, mMediaDevice4);
+ assertThat(
+ mOutputMediaItemListProxy
+ .getOutputMediaItemList()
+ .get(0)
+ .isFirstDeviceInGroup())
+ .isTrue();
+ } else {
+ // When the device grouping is disabled, the order of selected devices are reverted:
+ // * a media item with the selected mMediaDevice1
+ // * a media item with the selected mMediaDevice3
+ // * a group divider for speakers and displays
+ // * a media item with the mMediaDevice4
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(mMediaDevice1, mMediaDevice3, null, mMediaDevice4);
+ }
+ }
+
+ @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void updateMediaDevices_withConnectNewDeviceMediaItem_shouldUpdateMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ // Create the initial output media item list with a connect new device media item.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice2, mMediaDevice3),
+ /* selectedDevices */ List.of(mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ mConnectNewDeviceMediaItem);
+
+ // Check the output media items to be
+ // * a media item with the selected mMediaDevice3
+ // * a group divider for suggested devices
+ // * a media item with the mMediaDevice2
+ // * a connect new device media item
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList())
+ .contains(mConnectNewDeviceMediaItem);
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(mMediaDevice3, null, mMediaDevice2, null);
+
+ // Update the output media item list without a connect new device media item.
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice2, mMediaDevice3),
+ /* selectedDevices */ List.of(mMediaDevice3),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+
+ // Check the output media items to be
+ // * a media item with the selected mMediaDevice3
+ // * a group divider for suggested devices
+ // * a media item with the mMediaDevice2
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList())
+ .doesNotContain(mConnectNewDeviceMediaItem);
+ assertThat(getMediaDevices(mOutputMediaItemListProxy.getOutputMediaItemList()))
+ .containsExactly(mMediaDevice3, null, mMediaDevice2);
+ }
+
+ @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void clearAndAddAll_shouldUpdateMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem1));
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()).containsExactly(mMediaItem1);
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+
+ mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem2));
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()).containsExactly(mMediaItem2);
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+ }
+
+ @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void clear_flagOn_shouldClearMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice1),
+ /* selectedDevices */ List.of(),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+
+ mOutputMediaItemListProxy.clear();
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+ }
+
+ @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void clear_flagOff_shouldClearMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem1));
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+
+ mOutputMediaItemListProxy.clear();
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+ }
+
+ @EnableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void removeMutingExpectedDevices_flagOn_shouldClearMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ mOutputMediaItemListProxy.updateMediaDevices(
+ /* devices= */ List.of(mMediaDevice1),
+ /* selectedDevices */ List.of(),
+ /* connectedMediaDevice= */ null,
+ /* needToHandleMutingExpectedDevice= */ false,
+ /* connectNewDeviceMediaItem= */ null);
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+
+ mOutputMediaItemListProxy.removeMutingExpectedDevices();
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+ }
+
+ @DisableFlags(Flags.FLAG_FIX_OUTPUT_MEDIA_ITEM_LIST_INDEX_OUT_OF_BOUNDS_EXCEPTION)
+ @Test
+ public void removeMutingExpectedDevices_flagOff_shouldClearMediaItemList() {
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isTrue();
+
+ mOutputMediaItemListProxy.clearAndAddAll(List.of(mMediaItem1));
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+
+ mOutputMediaItemListProxy.removeMutingExpectedDevices();
+ assertThat(mOutputMediaItemListProxy.getOutputMediaItemList()).containsExactly(mMediaItem1);
+ assertThat(mOutputMediaItemListProxy.isEmpty()).isFalse();
+ }
+
+ private List<MediaDevice> getMediaDevices(List<MediaItem> mediaItems) {
+ return mediaItems.stream()
+ .map(item -> item.getMediaDevice().orElse(null))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index a71224a68125..23166a800245 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -21,6 +21,7 @@ import static android.view.MotionEvent.BUTTON_SECONDARY;
import static android.view.accessibility.AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT;
import static android.view.accessibility.AccessibilityManager.AUTOCLICK_DELAY_DEFAULT;
import static android.view.accessibility.AccessibilityManager.AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT;
+import static android.view.accessibility.AccessibilityManager.AUTOCLICK_REVERT_TO_LEFT_CLICK_DEFAULT;
import static com.android.server.accessibility.autoclick.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK;
@@ -159,7 +160,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
initiateAutoclickIndicator(handler);
}
- mClickScheduler = new ClickScheduler(handler, AUTOCLICK_DELAY_DEFAULT);
+ mClickScheduler = new ClickScheduler(
+ handler, AUTOCLICK_DELAY_DEFAULT);
mAutoclickSettingsObserver = new AutoclickSettingsObserver(mUserId, handler);
mAutoclickSettingsObserver.start(
mContext.getContentResolver(),
@@ -304,6 +306,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
Settings.Secure.getUriFor(
Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT);
+ private final Uri mAutoclickRevertToLeftClickSettingUri =
+ Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK);
+
private ContentResolver mContentResolver;
private ClickScheduler mClickScheduler;
private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
@@ -368,6 +374,13 @@ public class AutoclickController extends BaseEventStreamTransformation {
/* observer= */ this,
mUserId);
onChange(/* selfChange= */ true, mAutoclickIgnoreMinorCursorMovementSettingUri);
+
+ mContentResolver.registerContentObserver(
+ mAutoclickRevertToLeftClickSettingUri,
+ /* notifyForDescendants= */ false,
+ /* observer= */ this,
+ mUserId);
+ onChange(/* selfChange= */ true, mAutoclickRevertToLeftClickSettingUri);
}
}
@@ -424,6 +437,20 @@ public class AutoclickController extends BaseEventStreamTransformation {
== AccessibilityUtils.State.ON;
mClickScheduler.setIgnoreMinorCursorMovement(ignoreMinorCursorMovement);
}
+
+ if (mAutoclickRevertToLeftClickSettingUri.equals(uri)) {
+ boolean revertToLeftClick =
+ Settings.Secure.getIntForUser(
+ mContentResolver,
+ Settings.Secure
+ .ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK,
+ AUTOCLICK_REVERT_TO_LEFT_CLICK_DEFAULT
+ ? AccessibilityUtils.State.ON
+ : AccessibilityUtils.State.OFF,
+ mUserId)
+ == AccessibilityUtils.State.ON;
+ mClickScheduler.setRevertToLeftClick(revertToLeftClick);
+ }
}
}
}
@@ -505,6 +532,9 @@ public class AutoclickController extends BaseEventStreamTransformation {
/** Whether the minor cursor movement should be ignored. */
private boolean mIgnoreMinorCursorMovement = AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT;
+ /** Whether the autoclick type reverts to left click once performing an action. */
+ private boolean mRevertToLeftClick = AUTOCLICK_REVERT_TO_LEFT_CLICK_DEFAULT;
+
/** Whether there is pending click. */
private boolean mActive;
/** If active, time at which pending click is scheduled. */
@@ -555,6 +585,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
sendClick();
resetInternalState();
+ resetSelectedClickTypeIfNecessary();
}
/**
@@ -633,6 +664,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
return mDelay;
}
+ @VisibleForTesting
+ boolean getRevertToLeftClickForTesting() {
+ return mRevertToLeftClick;
+ }
+
/**
* Updates the time at which click sequence should occur.
*
@@ -692,6 +728,12 @@ public class AutoclickController extends BaseEventStreamTransformation {
}
}
+ private void resetSelectedClickTypeIfNecessary() {
+ if (mRevertToLeftClick && mActiveClickType != AUTOCLICK_TYPE_LEFT_CLICK) {
+ mAutoclickTypePanel.resetSelectedClickType();
+ }
+ }
+
/**
* @param event Observed motion event.
* @return Whether the event coords are far enough from the anchor for the event not to be
@@ -716,6 +758,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
mIgnoreMinorCursorMovement = ignoreMinorCursorMovement;
}
+ public void setRevertToLeftClick(boolean revertToLeftClick) {
+ mRevertToLeftClick = revertToLeftClick;
+ }
+
private void updateMovementSlop(double slop) {
mMovementSlop = slop;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
index 57fa77d73729..5a484d42eb96 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
@@ -283,7 +283,11 @@ public class AutoclickTypePanel {
// The pause button calls `togglePause()` directly so it does not need extra logic.
mPauseButton.setOnClickListener(v -> togglePause());
- // Initializes panel as collapsed state and only displays the left click button.
+ resetSelectedClickType();
+ }
+
+ /** Reset panel as collapsed state and only displays the left click button. */
+ public void resetSelectedClickType() {
hideAllClickTypeButtons();
mLeftClickButton.setVisibility(View.VISIBLE);
setSelectedClickType(AUTOCLICK_TYPE_LEFT_CLICK);
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 81d34f67dee9..fe4fa1068bb3 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -41,6 +41,7 @@ import dalvik.annotation.optimization.NeverCompile;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeUnit;
/**
* Tunable parameters for broadcast dispatch policy
@@ -286,6 +287,17 @@ public class BroadcastConstants {
"max_frozen_outgoing_broadcasts";
private static final int DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS = 32;
+ /**
+ * For {@link BroadcastQueueImpl}: Indicates how long after a process start was initiated,
+ * it should be considered abandoned and discarded.
+ */
+ public long PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS =
+ DEFAULT_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS * Build.HW_TIMEOUT_MULTIPLIER;
+ private static final String KEY_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS =
+ "pending_cold_start_abandon_timeout_millis";
+ private static final long DEFAULT_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS =
+ TimeUnit.MINUTES.toMillis(5);
+
// Settings override tracking for this instance
private String mSettingsKey;
private SettingsObserver mSettingsObserver;
@@ -434,6 +446,10 @@ public class BroadcastConstants {
MAX_FROZEN_OUTGOING_BROADCASTS = getDeviceConfigInt(
KEY_MAX_FROZEN_OUTGOING_BROADCASTS,
DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS);
+ PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = getDeviceConfigLong(
+ KEY_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS,
+ DEFAULT_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS)
+ * Build.HW_TIMEOUT_MULTIPLIER;
}
// TODO: migrate BroadcastRecord to accept a BroadcastConstants
@@ -491,6 +507,8 @@ public class BroadcastConstants {
PENDING_COLD_START_CHECK_INTERVAL_MILLIS).println();
pw.print(KEY_MAX_FROZEN_OUTGOING_BROADCASTS,
MAX_FROZEN_OUTGOING_BROADCASTS).println();
+ pw.print(KEY_PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS,
+ PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS).println();
pw.decreaseIndent();
pw.println();
}
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 508c01802156..c0fe73877c01 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -245,6 +245,24 @@ class BroadcastProcessQueue {
*/
private final ArrayList<BroadcastRecord> mOutgoingBroadcasts = new ArrayList<>();
+ /**
+ * The timestamp, in {@link SystemClock#uptimeMillis()}, at which a cold start was initiated
+ * for the process associated with this queue.
+ *
+ * Note: We could use the already existing {@link ProcessRecord#getStartUptime()} instead
+ * of this, but the need for this timestamp is to identify an issue (b/393898613) where the
+ * suspicion is that process is not attached or getting changed. So, we don't want to rely on
+ * ProcessRecord directly for this purpose.
+ */
+ private long mProcessStartInitiatedTimestampMillis;
+
+ /**
+ * Indicates whether the number of current receivers has been incremented using
+ * {@link ProcessReceiverRecord#incrementCurReceivers()}. This allows to skip decrementing
+ * the receivers when it is not required.
+ */
+ private boolean mCurReceiversIncremented;
+
public BroadcastProcessQueue(@NonNull BroadcastConstants constants,
@NonNull String processName, int uid) {
this.constants = Objects.requireNonNull(constants);
@@ -652,6 +670,52 @@ class BroadcastProcessQueue {
return mActiveFirstLaunch;
}
+ public void incrementCurAppReceivers() {
+ app.mReceivers.incrementCurReceivers();
+ mCurReceiversIncremented = true;
+ }
+
+ public void decrementCurAppReceivers() {
+ if (mCurReceiversIncremented) {
+ app.mReceivers.decrementCurReceivers();
+ mCurReceiversIncremented = false;
+ }
+ }
+
+ public void setProcessStartInitiatedTimestampMillis(@UptimeMillisLong long timestampMillis) {
+ mProcessStartInitiatedTimestampMillis = timestampMillis;
+ }
+
+ @UptimeMillisLong
+ public long getProcessStartInitiatedTimestampMillis() {
+ return mProcessStartInitiatedTimestampMillis;
+ }
+
+ public boolean hasProcessStartInitiationTimedout() {
+ if (mProcessStartInitiatedTimestampMillis <= 0) {
+ return false;
+ }
+ return (SystemClock.uptimeMillis() - mProcessStartInitiatedTimestampMillis)
+ > constants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS;
+ }
+
+ /**
+ * Returns if the process start initiation is expected to be timed out at this point. This
+ * allows us to dump necessary state for debugging before the process start is timed out
+ * and discarded.
+ */
+ public boolean isProcessStartInitiationTimeoutExpected() {
+ if (mProcessStartInitiatedTimestampMillis <= 0) {
+ return false;
+ }
+ return (SystemClock.uptimeMillis() - mProcessStartInitiatedTimestampMillis)
+ > constants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS / 2;
+ }
+
+ public void clearProcessStartInitiatedTimestampMillis() {
+ mProcessStartInitiatedTimestampMillis = 0;
+ }
+
/**
* Get package name of the first application loaded into this process.
*/
@@ -1558,6 +1622,10 @@ class BroadcastProcessQueue {
if (mActiveReEnqueued) {
pw.print("activeReEnqueued:"); pw.println(mActiveReEnqueued);
}
+ if (mProcessStartInitiatedTimestampMillis > 0) {
+ pw.print("processStartInitiatedTimestamp:"); pw.println(
+ TimeUtils.formatUptime(mProcessStartInitiatedTimestampMillis));
+ }
}
@NeverCompile
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index d276b9a94791..6e893ad0a425 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -534,6 +534,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
// skip to look for another warm process
if (mRunningColdStart == null) {
mRunningColdStart = queue;
+ mRunningColdStart.clearProcessStartInitiatedTimestampMillis();
} else if (isPendingColdStartValid()) {
// Move to considering next runnable queue
queue = nextQueue;
@@ -542,6 +543,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
// Pending cold start is not valid, so clear it and move on.
clearInvalidPendingColdStart();
mRunningColdStart = queue;
+ mRunningColdStart.clearProcessStartInitiatedTimestampMillis();
}
}
@@ -588,7 +590,9 @@ class BroadcastQueueImpl extends BroadcastQueue {
@GuardedBy("mService")
private boolean isPendingColdStartValid() {
- if (mRunningColdStart.app.getPid() > 0) {
+ if (mRunningColdStart.hasProcessStartInitiationTimedout()) {
+ return false;
+ } else if (mRunningColdStart.app.getPid() > 0) {
// If the process has already started, check if it wasn't killed.
return !mRunningColdStart.app.isKilled();
} else {
@@ -673,6 +677,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
if ((mRunningColdStart != null) && (mRunningColdStart == queue)) {
// We've been waiting for this app to cold start, and it's ready
// now; dispatch its next broadcast and clear the slot
+ mRunningColdStart.clearProcessStartInitiatedTimestampMillis();
mRunningColdStart = null;
// Now that we're running warm, we can finally request that OOM
@@ -756,6 +761,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
// We've been waiting for this app to cold start, and it had
// trouble; clear the slot and fail delivery below
+ mRunningColdStart.clearProcessStartInitiatedTimestampMillis();
mRunningColdStart = null;
// We might be willing to kick off another cold start
@@ -1036,6 +1042,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
"startProcessLocked failed");
return true;
}
+ queue.setProcessStartInitiatedTimestampMillis(SystemClock.uptimeMillis());
// TODO: b/335420031 - cache receiver intent to avoid multiple calls to getReceiverIntent.
mService.mProcessList.getAppStartInfoTracker().handleProcessBroadcastStart(
startTimeNs, queue.app, r.getReceiverIntent(receiver), r.alarm /* isAlarm */);
@@ -1991,6 +1998,32 @@ class BroadcastQueueImpl extends BroadcastQueue {
if (mRunningColdStart != null) {
checkState(getRunningIndexOf(mRunningColdStart) >= 0,
"isOrphaned " + mRunningColdStart);
+
+ final BroadcastProcessQueue queue = getProcessQueue(mRunningColdStart.processName,
+ mRunningColdStart.uid);
+ checkState(queue == mRunningColdStart, "Conflicting " + mRunningColdStart
+ + " with queue " + queue
+ + ";\n mRunningColdStart.app: " + mRunningColdStart.app.toDetailedString()
+ + ";\n queue.app: " + queue.app.toDetailedString());
+
+ checkState(mRunningColdStart.app != null, "Empty cold start queue "
+ + mRunningColdStart);
+
+ if (mRunningColdStart.isProcessStartInitiationTimeoutExpected()) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("Process start timeout expected for app ");
+ sb.append(mRunningColdStart.app);
+ sb.append(" in queue ");
+ sb.append(mRunningColdStart);
+ sb.append("; startUpTime: ");
+ final long startupTimeMs =
+ mRunningColdStart.getProcessStartInitiatedTimestampMillis();
+ sb.append(startupTimeMs == 0 ? "<none>"
+ : TimeUtils.formatDuration(startupTimeMs - SystemClock.uptimeMillis()));
+ sb.append(";\n app: ");
+ sb.append(mRunningColdStart.app.toDetailedString());
+ checkState(false, sb.toString());
+ }
}
// Verify health of all known process queues
@@ -2090,7 +2123,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
@GuardedBy("mService")
private void notifyStartedRunning(@NonNull BroadcastProcessQueue queue) {
if (queue.app != null) {
- queue.app.mReceivers.incrementCurReceivers();
+ queue.incrementCurAppReceivers();
// Don't bump its LRU position if it's in the background restricted.
if (mService.mInternal.getRestrictionLevel(
@@ -2115,7 +2148,7 @@ class BroadcastQueueImpl extends BroadcastQueue {
@GuardedBy("mService")
private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) {
if (queue.app != null) {
- queue.app.mReceivers.decrementCurReceivers();
+ queue.decrementCurAppReceivers();
if (queue.runningOomAdjusted) {
mService.enqueueOomAdjTargetLocked(queue.app);
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index eea667ef2f39..400c699bf93f 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -70,6 +70,7 @@ import com.android.server.wm.WindowProcessController;
import com.android.server.wm.WindowProcessListener;
import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
@@ -1414,6 +1415,16 @@ class ProcessRecord implements WindowProcessListener {
return mStringName = sb.toString();
}
+ String toDetailedString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(this);
+ final StringWriter sw = new StringWriter();
+ final PrintWriter pw = new PrintWriter(sw);
+ dump(pw, " ");
+ sb.append(sw);
+ return sb.toString();
+ }
+
/*
* Return true if package has been added false if not
*/
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index cfd22fbdeece..cb4342f27bc8 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -253,11 +253,12 @@ public class SettingsToPropertiesMapper {
"pixel_state_server",
"pixel_system_sw_video",
"pixel_video_sw",
+ "pixel_vpn",
"pixel_watch",
+ "pixel_watch_debug_trace",
"pixel_wifi",
"platform_compat",
"platform_security",
- "pixel_watch_debug_trace",
"pmw",
"power",
"preload_safety",
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index 0e1fbf3a6d1a..f50f45a182d0 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -118,7 +118,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
@Override
void shutdown() {
mSqliteWriteHandler.removeAllPendingMessages();
- mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents());
}
@Override
@@ -172,10 +172,14 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
@Nullable String[] opNamesFilter,
@Nullable String attributionTagFilter, int opFlagsFilter,
Set<String> attributionExemptPkgs) {
+ IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
// flush the cache into database before read.
- mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
+ if (opCodes != null) {
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAppOpEvents(opCodes));
+ } else {
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents());
+ }
boolean assembleChains = attributionExemptPkgs != null;
- IntArray opCodes = getAppOpCodes(filter, opNamesFilter);
beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff,
ChronoUnit.MILLIS).toEpochMilli());
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
@@ -214,7 +218,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
@NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
int nDiscreteOps) {
// flush the cache into database before dump.
- mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.evictAllAppOpEvents());
IntArray opCodes = new IntArray();
if (dumpOp != AppOpsManager.OP_NONE) {
opCodes.add(dumpOp);
@@ -366,7 +370,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
try {
List<DiscreteOp> evictedEvents;
synchronized (mDiscreteOpCache) {
- evictedEvents = mDiscreteOpCache.evict();
+ evictedEvents = mDiscreteOpCache.evictOldAppOpEvents();
}
mDiscreteOpsDbHelper.insertDiscreteOps(evictedEvents);
} finally {
@@ -389,7 +393,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
try {
List<DiscreteOp> evictedEvents;
synchronized (mDiscreteOpCache) {
- evictedEvents = mDiscreteOpCache.evict();
+ evictedEvents = mDiscreteOpCache.evictOldAppOpEvents();
// if nothing to evict, just write the whole cache to database.
if (evictedEvents.isEmpty()
&& mDiscreteOpCache.size() >= mDiscreteOpCache.capacity()) {
@@ -451,9 +455,10 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
}
/**
- * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization}.
+ * Evict entries older than {@link DiscreteOpsRegistry#sDiscreteHistoryQuantization} i.e.
+ * app op events older than one minute (default quantization) will be evicted.
*/
- private List<DiscreteOp> evict() {
+ private List<DiscreteOp> evictOldAppOpEvents() {
synchronized (this) {
List<DiscreteOp> evictedEvents = new ArrayList<>();
Set<DiscreteOp> snapshot = new ArraySet<>(mCache);
@@ -470,11 +475,9 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
}
/**
- * Remove all the entries from cache.
- *
- * @return return all removed entries.
+ * Evict all app op entries from cache, and return the list of removed ops.
*/
- public List<DiscreteOp> getAllEventsAndClear() {
+ public List<DiscreteOp> evictAllAppOpEvents() {
synchronized (this) {
List<DiscreteOp> cachedOps = new ArrayList<>(mCache.size());
if (mCache.isEmpty()) {
@@ -486,6 +489,25 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
}
}
+ /**
+ * Evict specified app ops from cache, and return the list of evicted ops.
+ */
+ public List<DiscreteOp> evictAppOpEvents(IntArray ops) {
+ synchronized (this) {
+ List<DiscreteOp> evictedOps = new ArrayList<>();
+ if (mCache.isEmpty()) {
+ return evictedOps;
+ }
+ for (DiscreteOp discreteOp: mCache) {
+ if (ops.contains(discreteOp.getOpCode())) {
+ evictedOps.add(discreteOp);
+ }
+ }
+ evictedOps.forEach(mCache::remove);
+ return evictedOps;
+ }
+ }
+
int size() {
return mCache.size();
}
@@ -646,7 +668,10 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
+ ", uidState=" + getUidStateName(mUidState)
+ ", chainId=" + mChainId
+ ", accessTime=" + mAccessTime
- + ", duration=" + mDuration + '}';
+ + ", mDiscretizedAccessTime=" + mDiscretizedAccessTime
+ + ", duration=" + mDuration
+ + ", mDiscretizedDuration=" + mDiscretizedDuration
+ + '}';
}
public int getUid() {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index cf598e89c988..62264dd73795 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -96,6 +96,7 @@ import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.ShellCommand;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
@@ -1564,6 +1565,12 @@ class PackageManagerShellCommand extends ShellCommand {
private int doRunInstall(final InstallParams params) throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
+ // Do not allow app installation if boot has not completed already
+ if (!SystemProperties.getBoolean("sys.boot_completed", false)) {
+ pw.println("Error: device is still booting.");
+ return 1;
+ }
+
int requestUserId = params.userId;
if (requestUserId != UserHandle.USER_ALL && requestUserId != UserHandle.USER_CURRENT) {
UserManagerInternal umi =
@@ -2174,6 +2181,13 @@ class PackageManagerShellCommand extends ShellCommand {
private int runUninstall() throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
+
+ // Do not allow app uninstallation if boot has not completed already
+ if (!SystemProperties.getBoolean("sys.boot_completed", false)) {
+ pw.println("Error: device is still booting.");
+ return 1;
+ }
+
int flags = 0;
int userId = UserHandle.USER_ALL;
long versionCode = PackageManager.VERSION_CODE_HIGHEST;
diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
index 799157520ca5..800fc7c25de5 100644
--- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
+++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
@@ -23,6 +23,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
+import android.os.Binder;
import android.os.Environment;
import android.util.AtomicFile;
import android.util.Slog;
@@ -119,7 +120,7 @@ class CertificateRevocationStatusManager {
} catch (IOException | JSONException ex) {
Slog.d(TAG, "Fallback to check stored revocation status", ex);
if (ex instanceof IOException && mShouldScheduleJob) {
- scheduleJobToFetchRemoteRevocationJob();
+ Binder.withCleanCallingIdentity(this::scheduleJobToFetchRemoteRevocationJob);
}
try {
revocationList = getStoredRevocationList();
@@ -210,7 +211,7 @@ class CertificateRevocationStatusManager {
return;
}
Slog.d(TAG, "Scheduling job to fetch remote CRL.");
- jobScheduler.schedule(
+ jobScheduler.forNamespace(TAG).schedule(
new JobInfo.Builder(
JOB_ID,
new ComponentName(
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e91d88901751..919b14b6db62 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -625,7 +625,7 @@ final class ActivityRecord extends WindowToken {
@VisibleForTesting
final TaskFragment.ConfigOverrideHint mResolveConfigHint;
- private final boolean mOptOutEdgeToEdge;
+ final boolean mOptOutEdgeToEdge;
private static ConstrainDisplayApisConfig sConstrainDisplayApisConfig;
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index b91a12598e01..80df081a6271 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -216,6 +216,7 @@ final class AppCompatUtils {
AppCompatCameraPolicy.getCameraCompatFreeformMode(top);
appCompatTaskInfo.setHasMinAspectRatioOverride(top.mAppCompatController
.getDesktopAspectRatioPolicy().hasMinAspectRatioOverride(task));
+ appCompatTaskInfo.setOptOutEdgeToEdge(top.mOptOutEdgeToEdge);
}
/**
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index 7a959c14fbd2..ce3ad889a308 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -24,6 +24,8 @@ import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx;
+import static com.android.internal.policy.DesktopModeCompatUtils.shouldExcludeCaptionFromAppBounds;
import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
@@ -64,7 +66,8 @@ public final class DesktopModeBoundsCalculator {
*/
static void updateInitialBounds(@NonNull Task task, @Nullable WindowLayout layout,
@Nullable ActivityRecord activity, @Nullable ActivityOptions options,
- @NonNull Rect outBounds, @NonNull Consumer<String> logger) {
+ @NonNull LaunchParamsController.LaunchParams outParams,
+ @NonNull Consumer<String> logger) {
// Use stable frame instead of raw frame to avoid launching freeform windows on top of
// stable insets, which usually are system widgets such as sysbar & navbar.
final Rect stableBounds = new Rect();
@@ -77,36 +80,44 @@ public final class DesktopModeBoundsCalculator {
// during the size update.
final boolean shouldRespectOptionPosition =
updateOptionBoundsSize && DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue();
+ final int captionHeight = activity != null && shouldExcludeCaptionFromAppBounds(
+ activity.info, task.isResizeable(), activity.mOptOutEdgeToEdge)
+ ? getDesktopViewAppHeaderHeightPx(activity.mWmService.mContext) : 0;
if (options != null && options.getLaunchBounds() != null
&& !updateOptionBoundsSize) {
- outBounds.set(options.getLaunchBounds());
- logger.accept("inherit-from-options=" + outBounds);
+ outParams.mBounds.set(options.getLaunchBounds());
+ logger.accept("inherit-from-options=" + outParams.mBounds);
} else if (layout != null) {
final int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (layout.hasSpecifiedSize()) {
- calculateLayoutBounds(stableBounds, layout, outBounds,
+ calculateLayoutBounds(stableBounds, layout, outParams.mBounds,
calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
- applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
+ applyLayoutGravity(verticalGravity, horizontalGravity, outParams.mBounds,
stableBounds);
logger.accept("layout specifies sizes, inheriting size and applying gravity");
} else if (verticalGravity > 0 || horizontalGravity > 0) {
- outBounds.set(calculateInitialBounds(task, activity, stableBounds, options,
- shouldRespectOptionPosition));
- applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
+ outParams.mBounds.set(calculateInitialBounds(task, activity, stableBounds, options,
+ shouldRespectOptionPosition, captionHeight));
+ applyLayoutGravity(verticalGravity, horizontalGravity, outParams.mBounds,
stableBounds);
logger.accept("layout specifies gravity, applying desired bounds and gravity");
logger.accept("respecting option bounds cascaded position="
+ shouldRespectOptionPosition);
}
} else {
- outBounds.set(calculateInitialBounds(task, activity, stableBounds, options,
- shouldRespectOptionPosition));
+ outParams.mBounds.set(calculateInitialBounds(task, activity, stableBounds, options,
+ shouldRespectOptionPosition, captionHeight));
logger.accept("layout not specified, applying desired bounds");
logger.accept("respecting option bounds cascaded position="
+ shouldRespectOptionPosition);
}
+ if (updateOptionBoundsSize && captionHeight != 0) {
+ outParams.mAppBounds.set(outParams.mBounds);
+ outParams.mAppBounds.top += captionHeight;
+ logger.accept("excluding caption height from app bounds");
+ }
}
/**
@@ -119,7 +130,8 @@ public final class DesktopModeBoundsCalculator {
@NonNull
private static Rect calculateInitialBounds(@NonNull Task task,
@NonNull ActivityRecord activity, @NonNull Rect stableBounds,
- @Nullable ActivityOptions options, boolean shouldRespectOptionPosition
+ @Nullable ActivityOptions options, boolean shouldRespectOptionPosition,
+ int captionHeight
) {
// Display bounds not taking into account insets.
final TaskDisplayArea displayArea = task.getDisplayArea();
@@ -160,7 +172,8 @@ public final class DesktopModeBoundsCalculator {
}
// If activity is unresizeable, regardless of orientation, calculate maximum size
// (within the ideal size) maintaining original aspect ratio.
- yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio);
+ yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio,
+ captionHeight);
}
case ORIENTATION_PORTRAIT -> {
// Device in portrait orientation.
@@ -188,11 +201,12 @@ public final class DesktopModeBoundsCalculator {
// ratio.
yield maximizeSizeGivenAspectRatio(activityOrientation,
new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()),
- appAspectRatio);
+ appAspectRatio, captionHeight);
}
// For portrait unresizeable activities, calculate maximum size (within the ideal
// size) maintaining original aspect ratio.
- yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio);
+ yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio,
+ captionHeight);
}
default -> idealSize;
};
@@ -232,13 +246,15 @@ public final class DesktopModeBoundsCalculator {
* Calculates the largest size that can fit in a given area while maintaining a specific aspect
* ratio.
*/
+ // TODO(b/400617906): Merge duplicate initial bounds calculations to shared class.
@NonNull
private static Size maximizeSizeGivenAspectRatio(
@ScreenOrientation int orientation,
@NonNull Size targetArea,
- float aspectRatio
+ float aspectRatio,
+ int captionHeight
) {
- final int targetHeight = targetArea.getHeight();
+ final int targetHeight = targetArea.getHeight() - captionHeight;
final int targetWidth = targetArea.getWidth();
final int finalHeight;
final int finalWidth;
@@ -275,7 +291,7 @@ public final class DesktopModeBoundsCalculator {
finalHeight = (int) (finalWidth / aspectRatio);
}
}
- return new Size(finalWidth, finalHeight);
+ return new Size(finalWidth, finalHeight + captionHeight);
}
/**
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index ddcb5eccb1d8..6698d2ec7cc4 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -150,7 +150,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
}
DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options,
- outParams.mBounds, this::appendLog);
+ outParams, this::appendLog);
appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
if (options != null && options.getFlexibleLaunchSize()) {
// Return result done to prevent other modifiers from respecting option bounds and
diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java
index fa65bda7104d..8eec98cb5fcc 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsController.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsController.java
@@ -26,6 +26,7 @@ import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.WindowConfiguration.WindowingMode;
@@ -138,6 +139,10 @@ class LaunchParamsController {
mService.deferWindowLayout();
try {
if (task.getRootTask().inMultiWindowMode()) {
+ if (!mTmpParams.mAppBounds.isEmpty()) {
+ task.getRequestedOverrideConfiguration().windowConfiguration.setAppBounds(
+ mTmpParams.mAppBounds);
+ }
task.setBounds(mTmpParams.mBounds);
return true;
}
@@ -169,6 +174,9 @@ class LaunchParamsController {
static class LaunchParams {
/** The bounds within the parent container. */
final Rect mBounds = new Rect();
+ /** The bounds within the parent container respecting insets. Usually empty. */
+ @NonNull
+ final Rect mAppBounds = new Rect();
/** The display area the {@link Task} would prefer to be on. */
@Nullable
@@ -181,6 +189,7 @@ class LaunchParamsController {
/** Sets values back to default. {@link #isEmpty} will return {@code true} once called. */
void reset() {
mBounds.setEmpty();
+ mAppBounds.setEmpty();
mPreferredTaskDisplayArea = null;
mWindowingMode = WINDOWING_MODE_UNDEFINED;
}
@@ -188,13 +197,14 @@ class LaunchParamsController {
/** Copies the values set on the passed in {@link LaunchParams}. */
void set(LaunchParams params) {
mBounds.set(params.mBounds);
+ mAppBounds.set(params.mAppBounds);
mPreferredTaskDisplayArea = params.mPreferredTaskDisplayArea;
mWindowingMode = params.mWindowingMode;
}
/** Returns {@code true} if no values have been explicitly set. */
boolean isEmpty() {
- return mBounds.isEmpty() && mPreferredTaskDisplayArea == null
+ return mBounds.isEmpty() && mAppBounds.isEmpty() && mPreferredTaskDisplayArea == null
&& mWindowingMode == WINDOWING_MODE_UNDEFINED;
}
@@ -215,12 +225,14 @@ class LaunchParamsController {
if (mPreferredTaskDisplayArea != that.mPreferredTaskDisplayArea) return false;
if (mWindowingMode != that.mWindowingMode) return false;
+ if (!mAppBounds.equals(that.mAppBounds)) return false;
return mBounds != null ? mBounds.equals(that.mBounds) : that.mBounds == null;
}
@Override
public int hashCode() {
int result = mBounds != null ? mBounds.hashCode() : 0;
+ result = 31 * result + mAppBounds.hashCode();
result = 31 * result + (mPreferredTaskDisplayArea != null
? mPreferredTaskDisplayArea.hashCode() : 0);
result = 31 * result + mWindowingMode;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ce91fc5baba1..d528776a2c25 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -100,7 +100,6 @@ import static android.view.WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
-import static com.android.input.flags.Flags.removeInputChannelFromWindowstate;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ADD_REMOVE;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_ANIM;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_APP_TRANSITIONS;
@@ -613,10 +612,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// Input channel and input window handle used by the input dispatcher.
final InputWindowHandleWrapper mInputWindowHandle;
- /**
- * Only populated if flag REMOVE_INPUT_CHANNEL_FROM_WINDOWSTATE is disabled.
- */
- private InputChannel mInputChannel;
/**
* The token will be assigned to {@link InputWindowHandle#token} if this window can receive
@@ -1830,12 +1825,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
* Input Manager uses when discarding windows from input consideration.
*/
boolean isPotentialDragTarget(boolean targetInterceptsGlobalDrag) {
- if (removeInputChannelFromWindowstate()) {
- return (targetInterceptsGlobalDrag || isVisibleNow()) && !mRemoved
- && mInputChannelToken != null && mInputWindowHandle != null;
- }
return (targetInterceptsGlobalDrag || isVisibleNow()) && !mRemoved
- && mInputChannel != null && mInputWindowHandle != null;
+ && mInputChannelToken != null && mInputWindowHandle != null;
}
/**
@@ -2583,25 +2574,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (mInputChannelToken != null) {
throw new IllegalStateException("Window already has an input channel token.");
}
- if (removeInputChannelFromWindowstate()) {
- String name = getName();
- InputChannel channel = mWmService.mInputManager.createInputChannel(name);
- mInputChannelToken = channel.getToken();
- mInputWindowHandle.setToken(mInputChannelToken);
- mWmService.mInputToWindowMap.put(mInputChannelToken, this);
- channel.copyTo(outInputChannel);
- channel.dispose();
- return;
- }
- if (mInputChannel != null) {
- throw new IllegalStateException("Window already has an input channel.");
- }
String name = getName();
- mInputChannel = mWmService.mInputManager.createInputChannel(name);
- mInputChannelToken = mInputChannel.getToken();
+ InputChannel channel = mWmService.mInputManager.createInputChannel(name);
+ mInputChannelToken = channel.getToken();
mInputWindowHandle.setToken(mInputChannelToken);
mWmService.mInputToWindowMap.put(mInputChannelToken, this);
- mInputChannel.copyTo(outInputChannel);
+ channel.copyTo(outInputChannel);
+ channel.dispose();
}
/**
@@ -2624,12 +2603,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mInputChannelToken = null;
}
- if (!removeInputChannelFromWindowstate()) {
- if (mInputChannel != null) {
- mInputChannel.dispose();
- mInputChannel = null;
- }
- }
mInputWindowHandle.setToken(null);
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index a8c49e11e4e9..e32ce525cb40 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2309,13 +2309,6 @@ static jint nativeGetKeyCodeForKeyLocation(JNIEnv* env, jobject nativeImplObj, j
locationKeyCode);
}
-static void handleInputChannelDisposed(JNIEnv* env, jobject /* inputChannelObj */,
- const std::shared_ptr<InputChannel>& inputChannel,
- void* data) {
- NativeInputManager* im = static_cast<NativeInputManager*>(data);
- im->removeInputChannel(inputChannel->getConnectionToken());
-}
-
static jobject nativeCreateInputChannel(JNIEnv* env, jobject nativeImplObj, jstring nameObj) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2337,8 +2330,6 @@ static jobject nativeCreateInputChannel(JNIEnv* env, jobject nativeImplObj, jstr
return nullptr;
}
- android_view_InputChannel_setDisposeCallback(env, inputChannelObj,
- handleInputChannelDisposed, im);
return inputChannelObj;
}
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java
index 7c239ef02e58..586ff52aa78c 100644
--- a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java
@@ -328,6 +328,7 @@ public class TestDreamEnvironment {
case DREAM_STATE_STARTED -> startDream();
case DREAM_STATE_WOKEN -> wakeDream();
}
+ mTestableLooper.processAllMessages();
} while (mCurrentDreamState < state);
return true;
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 2a513ae3a8e8..d79d88400cf9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -953,11 +953,13 @@ public final class AlarmManagerServiceTest {
@Test
@EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND)
- public void testWakelockOrdering() throws Exception {
+ public void testWakelockOrderingFirstAlarm() throws Exception {
final long triggerTime = mNowElapsedTest + 5000;
final PendingIntent alarmPi = getNewMockPendingIntent();
setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi);
+ // Pretend that it is the first alarm in this batch, or no other alarms are still processing
+ mService.mBroadcastRefCount = 0;
mNowElapsedTest = mTestTimer.getElapsed();
mTestTimer.expire();
@@ -975,20 +977,51 @@ public final class AlarmManagerServiceTest {
@Test
@EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND)
- public void testWakelockReleasedWhenSendFails() throws Exception {
+ public void testWakelockOrderingNonFirst() throws Exception {
final long triggerTime = mNowElapsedTest + 5000;
final PendingIntent alarmPi = getNewMockPendingIntent();
setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi);
+ // Pretend that some previous alarms are still processing.
+ mService.mBroadcastRefCount = 3;
+ mNowElapsedTest = mTestTimer.getElapsed();
+ mTestTimer.expire();
+
+ final ArgumentCaptor<PendingIntent.OnFinished> onFinishedCaptor =
+ ArgumentCaptor.forClass(PendingIntent.OnFinished.class);
+ verify(alarmPi).send(eq(mMockContext), eq(0), any(Intent.class), onFinishedCaptor.capture(),
+ any(Handler.class), isNull(), any());
+ onFinishedCaptor.getValue().onSendFinished(alarmPi, null, 0, null, null);
+
+ verify(mWakeLock, never()).acquire();
+ verify(mWakeLock, never()).release();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND)
+ public void testWakelockReleasedWhenSendFails() throws Exception {
+ final PendingIntent alarmPi = getNewMockPendingIntent();
doThrow(new PendingIntent.CanceledException("test")).when(alarmPi).send(eq(mMockContext),
eq(0), any(Intent.class), any(), any(Handler.class), isNull(), any());
+ setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5000, alarmPi);
+
+ // Pretend that it is the first alarm in this batch, or no other alarms are still processing
+ mService.mBroadcastRefCount = 0;
mNowElapsedTest = mTestTimer.getElapsed();
mTestTimer.expire();
final InOrder inOrder = Mockito.inOrder(mWakeLock);
inOrder.verify(mWakeLock).acquire();
inOrder.verify(mWakeLock).release();
+
+ setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5000, alarmPi);
+
+ // Pretend that some previous alarms are still processing.
+ mService.mBroadcastRefCount = 4;
+ mNowElapsedTest = mTestTimer.getElapsed();
+ mTestTimer.expire();
+ inOrder.verifyNoMoreInteractions();
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 3a9c99d57d71..d540b2ec13eb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -155,7 +155,6 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest {
doAnswer((invocation) -> {
Log.v(TAG, "Intercepting startProcessLocked() for "
+ Arrays.toString(invocation.getArguments()));
- assertHealth();
final String processName = invocation.getArgument(0);
final ProcessStartBehavior behavior = mNewProcessStartBehaviors.getOrDefault(
processName, mNextProcessStartBehavior.getAndSet(ProcessStartBehavior.SUCCESS));
@@ -206,6 +205,9 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest {
mActiveProcesses.remove(res);
res.setKilled(true);
break;
+ case MISSING_RESPONSE:
+ res.setPendingStart(true);
+ break;
default:
throw new UnsupportedOperationException();
}
@@ -244,6 +246,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest {
mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
mConstants.MAX_FROZEN_OUTGOING_BROADCASTS = 10;
+ mConstants.PENDING_COLD_START_ABANDON_TIMEOUT_MILLIS = 2000;
}
@After
@@ -279,6 +282,8 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest {
FAIL_NULL,
/** Process is killed without reporting to BroadcastQueue */
KILLED_WITHOUT_NOTIFY,
+ /** Process start fails without no response */
+ MISSING_RESPONSE,
}
private enum ProcessBehavior {
@@ -1173,6 +1178,37 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest {
verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
}
+ @Test
+ public void testProcessStartWithMissingResponse() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ mNewProcessStartBehaviors.put(PACKAGE_GREEN, ProcessStartBehavior.MISSING_RESPONSE);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, List.of(
+ withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
+ withPriority(makeRegisteredReceiver(receiverBlueApp), 5),
+ withPriority(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW), 0))));
+
+ final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
+ enqueueBroadcast(makeBroadcastRecord(timezone, callerApp,
+ List.of(makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE))));
+
+ waitForIdle();
+ final ProcessRecord receiverGreenApp = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+
+ verifyScheduleReceiver(times(1), receiverGreenApp, airplane);
+ verifyScheduleRegisteredReceiver(times(1), receiverBlueApp, airplane);
+ verifyScheduleReceiver(times(1), receiverYellowApp, airplane);
+ verifyScheduleReceiver(times(1), receiverOrangeApp, timezone);
+ }
+
/**
* Verify that a broadcast sent to a frozen app, which gets killed as part of unfreezing
* process due to pending sync binder transactions, is delivered as expected.
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 9cfa51a85988..8253595a50d1 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -2374,14 +2374,6 @@ public class AccessibilityManagerServiceTest {
return lockState;
}
- private void assertStartActivityWithExpectedComponentName(Context mockContext,
- String componentName) {
- verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(),
- any(Bundle.class), any(UserHandle.class));
- assertThat(mIntentArgumentCaptor.getValue().getStringExtra(
- Intent.EXTRA_COMPONENT_NAME)).isEqualTo(componentName);
- }
-
private void assertStartActivityWithExpectedShortcutType(Context mockContext,
@UserShortcutType int shortcutType) {
verify(mockContext).startActivityAsUser(mIntentArgumentCaptor.capture(),
@@ -2484,10 +2476,6 @@ public class AccessibilityManagerServiceTest {
return mMockContext;
}
- public void addMockUserContext(int userId, Context context) {
- mMockUserContexts.put(userId, context);
- }
-
@Override
@NonNull
public Context createContextAsUser(UserHandle user, int flags) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index ea25e7992dd9..2be43c6f21a5 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK;
import static com.android.server.testutils.MockitoUtilsKt.eq;
import static com.google.common.truth.Truth.assertThat;
@@ -547,6 +548,29 @@ public class AutoclickControllerTest {
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void triggerRightClickWithRevertToLeftClickEnabled_resetClickType() {
+ // Move mouse to initialize autoclick panel.
+ injectFakeMouseActionHoverMoveEvent();
+
+ AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
+ mController.mAutoclickTypePanel = mockAutoclickTypePanel;
+ mController.clickPanelController.handleAutoclickTypeChange(AUTOCLICK_TYPE_RIGHT_CLICK);
+
+ // Set ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK to true.
+ Settings.Secure.putIntForUser(mTestableContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK,
+ AccessibilityUtils.State.ON,
+ mTestableContext.getUserId());
+ mController.onChangeForTesting(/* selfChange= */ true,
+ Settings.Secure.getUriFor(
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_REVERT_TO_LEFT_CLICK));
+ when(mockAutoclickTypePanel.isPaused()).thenReturn(false);
+ mController.mClickScheduler.run();
+ assertThat(mController.mClickScheduler.getRevertToLeftClickForTesting()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void pauseButton_flagOn_clickNotTriggeredWhenPaused() {
injectFakeMouseActionHoverMoveEvent();
@@ -766,6 +790,8 @@ public class AutoclickControllerTest {
// Set click type to right click.
mController.clickPanelController.handleAutoclickTypeChange(
AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+ AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
+ mController.mAutoclickTypePanel = mockAutoclickTypePanel;
// Send hover move event.
MotionEvent hoverMove = MotionEvent.obtain(
diff --git a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java
index 01fee7f66497..918159f9262b 100644
--- a/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java
+++ b/services/tests/servicestests/src/com/android/server/appop/DiscreteAppOpSqlPersistenceTest.java
@@ -97,7 +97,8 @@ public class DiscreteAppOpSqlPersistenceTest {
mDiscreteRegistry.recordDiscreteAccess(opEvent2);
List<DiscreteOp> discreteOps = mDiscreteRegistry.getAllDiscreteOps();
- assertThat(discreteOps.size()).isEqualTo(1);
+ assertWithMessage("Expected list size is 1, but the list is: " + discreteOps)
+ .that(discreteOps.size()).isEqualTo(1);
assertThat(discreteOps).contains(opEvent);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index e87e107cd793..3e86f7b57294 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -41,6 +41,7 @@ import static android.util.DisplayMetrics.DENSITY_DEFAULT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx;
import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING;
import static com.android.server.wm.DesktopModeBoundsCalculator.centerInScreen;
@@ -893,9 +894,11 @@ public class DesktopModeLaunchParamsModifierTests extends
@Test
@EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS,
+ Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS})
public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() {
setupDesktopModeLaunchParamsModifier();
+ final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext);
final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
LANDSCAPE_DISPLAY_BOUNDS);
@@ -903,11 +906,11 @@ public class DesktopModeLaunchParamsModifierTests extends
final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_LANDSCAPE,
task, /* ignoreOrientationRequest */ true);
-
- final int desiredWidth =
- (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final float displayAspectRatio = (float) LANDSCAPE_DISPLAY_BOUNDS.width()
+ / LANDSCAPE_DISPLAY_BOUNDS.height();
final int desiredHeight =
(int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredWidth = (int) ((desiredHeight - captionHeight) * displayAspectRatio);
assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task)
.setActivity(activity).calculate());
@@ -916,7 +919,8 @@ public class DesktopModeLaunchParamsModifierTests extends
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ @EnableFlags({Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS,
+ Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS})
public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() {
setupDesktopModeLaunchParamsModifier();
@@ -925,6 +929,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final Task task = createTask(display, /* isResizeable */ false);
final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_PORTRAIT,
task, /* ignoreOrientationRequest */ true);
+ final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext);
spyOn(activity.mAppCompatController.getDesktopAspectRatioPolicy());
doReturn(LETTERBOX_ASPECT_RATIO).when(activity.mAppCompatController
@@ -932,7 +937,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final int desiredHeight =
(int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
- final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO);
+ final int desiredWidth = (int) ((desiredHeight - captionHeight) / LETTERBOX_ASPECT_RATIO);
assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task)
.setActivity(activity).calculate());
@@ -1070,7 +1075,8 @@ public class DesktopModeLaunchParamsModifierTests extends
@Test
@EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
- Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+ Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS,
+ Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS})
public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() {
setupDesktopModeLaunchParamsModifier();
@@ -1079,12 +1085,14 @@ public class DesktopModeLaunchParamsModifierTests extends
final Task task = createTask(display, /* isResizeable */ false);
final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_PORTRAIT,
task, /* ignoreOrientationRequest */ true);
+ final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext);
-
- final int desiredWidth =
- (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final float displayAspectRatio = (float) PORTRAIT_DISPLAY_BOUNDS.height()
+ / PORTRAIT_DISPLAY_BOUNDS.width();
final int desiredHeight =
- (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+ final int desiredWidth =
+ (int) ((desiredHeight - captionHeight) / displayAspectRatio);
assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task)
.setActivity(activity).calculate());
@@ -1093,7 +1101,8 @@ public class DesktopModeLaunchParamsModifierTests extends
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ @EnableFlags({Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS,
+ Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS})
public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() {
setupDesktopModeLaunchParamsModifier();
@@ -1102,6 +1111,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final Task task = createTask(display, /* isResizeable */ false);
final ActivityRecord activity = createActivity(display, SCREEN_ORIENTATION_LANDSCAPE,
task, /* ignoreOrientationRequest */ true);
+ final int captionHeight = getDesktopViewAppHeaderHeightPx(mContext);
spyOn(activity.mAppCompatController.getDesktopAspectRatioPolicy());
doReturn(LETTERBOX_ASPECT_RATIO).when(activity.mAppCompatController
@@ -1109,7 +1119,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
- (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
- final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO);
+ final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO) + captionHeight;
assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task)
.setActivity(activity).calculate());
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
index 67a95de8a5c1..0d5828a3c4e1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
@@ -44,6 +44,7 @@ import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.pm.ActivityInfo.WindowLayout;
import android.graphics.Rect;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.util.ArrayMap;
import android.util.SparseArray;
@@ -52,6 +53,7 @@ import androidx.test.filters.MediumTest;
import com.android.server.wm.LaunchParamsController.LaunchParams;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
+import com.android.window.flags.Flags;
import org.junit.Before;
import org.junit.Test;
@@ -372,6 +374,34 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
assertEquals(expected, task.mLastNonFullscreenBounds);
}
+ /**
+ * Ensures that app bounds are set to exclude freeform caption if window is in freeform.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS)
+ public void testLayoutTaskBoundsFreeformAppBounds() {
+ final Rect expected = new Rect(10, 20, 30, 40);
+
+ final LaunchParams params = new LaunchParams();
+ params.mBounds.set(expected);
+ params.mAppBounds.set(expected);
+ final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
+ final Task task = new TaskBuilder(mAtm.mTaskSupervisor)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ final ActivityOptions options = ActivityOptions.makeBasic().setFlexibleLaunchSize(true);
+
+ mController.registerModifier(positioner);
+
+ assertNotEquals(expected, task.getBounds());
+
+ layoutTask(task, options);
+
+ // Task will make adjustments to requested bounds. We only need to guarantee that the
+ // requested bounds are expected.
+ assertEquals(expected,
+ task.getRequestedOverrideConfiguration().windowConfiguration.getAppBounds());
+ }
+
public static class InstrumentedPositioner implements LaunchParamsModifier {
private final int mReturnVal;
@@ -473,4 +503,9 @@ public class LaunchParamsControllerTests extends WindowTestsBase {
mController.layoutTask(task, null /* layout */, null /* activity */, null /* source */,
null /* options */);
}
+
+ private void layoutTask(@NonNull Task task, ActivityOptions options) {
+ mController.layoutTask(task, null /* layout */, null /* activity */, null /* source */,
+ options /* options */);
+ }
}
diff --git a/tests/Input/assets/testPointerScale.png b/tests/Input/assets/testPointerScale.png
index 54d37c24afc6..781df47a5e24 100644
--- a/tests/Input/assets/testPointerScale.png
+++ b/tests/Input/assets/testPointerScale.png
Binary files differ
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index 649241aaaa8c..6b099450064f 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -238,6 +238,36 @@ public class TestableLooper {
while (processQueuedMessages() != 0) ;
}
+ public long peekWhen() {
+ if (isAtLeastBaklava()) {
+ return peekWhenBaklava();
+ } else {
+ return peekWhenLegacy();
+ }
+ }
+
+ private long peekWhenBaklava() {
+ Long when = mQueueWrapper.peekWhen();
+ if (when != null) {
+ return when;
+ } else {
+ return 0;
+ }
+ }
+
+ private long peekWhenLegacy() {
+ try {
+ Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue());
+ if (msg != null) {
+ return msg.getWhen();
+ } else {
+ return 0;
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Access failed in TestableLooper: set - Message.when", e);
+ }
+ }
+
public void moveTimeForward(long milliSeconds) {
if (isAtLeastBaklava()) {
moveTimeForwardBaklava(milliSeconds);