summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java34
-rw-r--r--core/java/android/appwidget/AppWidgetManager.java22
-rw-r--r--core/java/android/content/pm/IShortcutService.aidl2
-rw-r--r--core/java/android/content/pm/PackageManager.java12
-rw-r--r--core/java/android/hardware/display/DisplayManager.java9
-rw-r--r--core/java/android/inputmethodservice/IInputMethodWrapper.java2
-rw-r--r--core/java/android/inputmethodservice/InputMethodService.java45
-rw-r--r--core/java/android/provider/CallLog.java12
-rw-r--r--core/java/android/service/wallpaper/WallpaperService.java19
-rw-r--r--core/java/android/view/ViewRootImpl.java10
-rw-r--r--core/java/android/view/WindowManager.java15
-rw-r--r--core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java6
-rw-r--r--core/java/android/view/inputmethod/InputMethod.java25
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java71
-rw-r--r--core/java/android/view/inputmethod/InputMethodSession.java9
-rw-r--r--core/java/android/widget/RemoteViews.java55
-rw-r--r--core/java/android/window/TaskFragmentOperation.java47
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java7
-rw-r--r--core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java4
-rw-r--r--core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java17
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl13
-rw-r--r--core/java/com/android/internal/widget/LockPatternUtils.java2
-rw-r--r--core/res/res/values/config.xml18
-rw-r--r--core/res/res/values/config_telephony.xml17
-rw-r--r--core/res/res/values/symbols.xml2
-rw-r--r--core/tests/coretests/src/android/content/TEST_MAPPING18
-rw-r--r--core/tests/coretests/src/android/view/contentcapture/TEST_MAPPING18
-rw-r--r--core/tests/coretests/src/android/view/contentprotection/TEST_MAPPING18
-rw-r--r--data/etc/privapp-permissions-platform.xml1
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java16
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java4
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java8
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java56
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt57
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt108
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt128
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt63
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java41
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.cpp32
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.h22
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp3
-rw-r--r--media/java/android/media/audiofx/Visualizer.java10
-rw-r--r--media/jni/android_media_MediaCodec.cpp2
-rw-r--r--packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java52
-rw-r--r--packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java37
-rw-r--r--packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java14
-rw-r--r--packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java7
-rw-r--r--packages/SettingsLib/res/drawable/ic_hotspot_auto.xml25
-rw-r--r--packages/SettingsLib/res/drawable/ic_hotspot_laptop.xml25
-rw-r--r--packages/SettingsLib/res/drawable/ic_hotspot_phone.xml25
-rw-r--r--packages/SettingsLib/res/drawable/ic_hotspot_tablet.xml25
-rw-r--r--packages/SettingsLib/res/drawable/ic_hotspot_watch.xml25
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java6
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java16
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java21
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java93
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java17
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java37
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java3
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java20
-rw-r--r--packages/Shell/AndroidManifest.xml1
-rw-r--r--packages/SystemUI/Android.bp17
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt95
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt42
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt111
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt162
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt150
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt449
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt80
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt71
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt90
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt141
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt214
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt72
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt176
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt419
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt194
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt159
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt59
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt66
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt74
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt41
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt91
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt49
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt138
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt49
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt23
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt (renamed from packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt)2
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt357
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt348
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt270
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt849
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt55
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt11
-rw-r--r--packages/SystemUI/compose/core/tests/Android.bp2
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt97
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt323
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt241
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt217
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt58
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt88
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt86
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt153
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt60
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt84
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt (renamed from packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt)20
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt58
-rw-r--r--packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt10
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt20
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt72
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt170
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt437
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt145
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt336
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt16
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt3
-rw-r--r--packages/SystemUI/ktfmt_includes.txt4
-rw-r--r--packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml1
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml23
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_filled_large_background.xml22
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_filled_large_text.xml22
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml23
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_outline.xml23
-rw-r--r--packages/SystemUI/res/color/qs_dialog_btn_outline_text.xml23
-rw-r--r--packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml31
-rw-r--r--packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml31
-rw-r--r--packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml47
-rw-r--r--packages/SystemUI/res/drawable/ic_person_outline.xml26
-rw-r--r--packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml2
-rw-r--r--packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml2
-rw-r--r--packages/SystemUI/res/drawable/media_output_icon_volume.xml3
-rw-r--r--packages/SystemUI/res/drawable/media_output_title_icon_area.xml8
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml29
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml39
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml25
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml25
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml30
-rw-r--r--packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml30
-rw-r--r--packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml3
-rw-r--r--packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml2
-rw-r--r--packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml3
-rw-r--r--packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml2
-rw-r--r--packages/SystemUI/res/layout/immersive_mode_cling.xml6
-rw-r--r--packages/SystemUI/res/layout/media_smartspace_recommendations.xml136
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_card_button.xml26
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_item_v2.xml89
-rw-r--r--packages/SystemUI/res/layout/privacy_dialog_v2.xml109
-rw-r--r--packages/SystemUI/res/layout/qs_tile_side_icon.xml3
-rw-r--r--packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml29
-rw-r--r--packages/SystemUI/res/values-ldrtl/dimens.xml20
-rw-r--r--packages/SystemUI/res/values/colors.xml3
-rw-r--r--packages/SystemUI/res/values/dimens.xml5
-rw-r--r--packages/SystemUI/res/values/ids.xml4
-rw-r--r--packages/SystemUI/res/values/strings.xml50
-rw-r--r--packages/SystemUI/res/values/styles.xml24
-rw-r--r--packages/SystemUI/res/xml/media_recommendation_collapsed.xml101
-rw-r--r--packages/SystemUI/res/xml/media_recommendation_expanded.xml123
-rw-r--r--packages/SystemUI/res/xml/media_recommendations_collapsed.xml (renamed from packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml)0
-rw-r--r--packages/SystemUI/res/xml/media_recommendations_expanded.xml (renamed from packages/SystemUI/res/xml/media_recommendations_view_expanded.xml)0
-rw-r--r--packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt8
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java8
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java6
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java3
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java1
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java61
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java46
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt85
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt196
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt81
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt168
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistory.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt148
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt177
-rw-r--r--packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java30
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/Flags.kt223
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt99
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java55
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt83
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt117
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt138
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt138
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenModel.kt (renamed from packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt)25
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt93
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt129
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt95
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java64
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java28
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt157
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt327
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt288
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt79
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt71
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt108
-rw-r--r--packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt150
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt664
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt65
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt367
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt539
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/ChevronImageView.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java63
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java110
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java26
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt72
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java65
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java48
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt299
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt111
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt309
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java45
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java590
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java67
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java73
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java327
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java167
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java41
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java56
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java70
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java109
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java217
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/tuner/TunerService.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java10
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt114
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt113
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt111
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt171
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt91
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt34
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt76
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt263
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt277
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java88
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt141
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt133
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt57
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt223
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt260
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt21
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt72
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt299
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt65
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt42
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt265
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt49
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java19
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt26
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt47
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt191
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt323
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt530
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt68
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt127
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt226
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt69
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt49
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt26
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt825
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt322
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt55
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt38
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt105
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt22
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt189
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt85
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt56
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt40
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt59
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt87
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt90
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java42
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt245
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java26
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt56
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java30
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java97
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java47
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java12
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt4
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt171
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java47
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt (renamed from packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt)21
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt12
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java5
-rw-r--r--services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java34
-rw-r--r--services/contentcapture/java/com/android/server/contentprotection/ContentProtectionConsentManager.java109
-rw-r--r--services/core/java/com/android/server/PersistentDataBlockService.java5
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java124
-rw-r--r--services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java20
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java2
-rw-r--r--services/core/java/com/android/server/am/PendingIntentRecord.java9
-rw-r--r--services/core/java/com/android/server/am/ProcessServiceRecord.java3
-rw-r--r--services/core/java/com/android/server/am/ServiceRecord.java128
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java2
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java15
-rw-r--r--services/core/java/com/android/server/camera/CameraServiceProxy.java24
-rw-r--r--services/core/java/com/android/server/display/BrightnessRangeController.java8
-rw-r--r--services/core/java/com/android/server/display/DisplayBrightnessState.java36
-rw-r--r--services/core/java/com/android/server/display/DisplayManagerService.java5
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController.java30
-rw-r--r--services/core/java/com/android/server/display/DisplayPowerController2.java49
-rw-r--r--services/core/java/com/android/server/display/HighBrightnessModeController.java43
-rw-r--r--services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java3
-rw-r--r--services/core/java/com/android/server/display/TEST_MAPPING21
-rw-r--r--services/core/java/com/android/server/display/brightness/BrightnessUtils.java11
-rw-r--r--services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java4
-rw-r--r--services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java13
-rw-r--r--services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java5
-rw-r--r--services/core/java/com/android/server/dreams/DreamController.java32
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java12
-rw-r--r--services/core/java/com/android/server/input/KeyboardMetricsCollector.java310
-rw-r--r--services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java4
-rw-r--r--services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java5
-rw-r--r--services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java5
-rw-r--r--services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java46
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java11
-rw-r--r--services/core/java/com/android/server/inputmethod/InputMethodManagerService.java44
-rw-r--r--services/core/java/com/android/server/locksettings/LockSettingsService.java4
-rw-r--r--services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java37
-rw-r--r--services/core/java/com/android/server/media/projection/mediaprojection.md5
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java40
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java58
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java150
-rw-r--r--services/core/java/com/android/server/pm/DefaultAppProvider.java32
-rw-r--r--services/core/java/com/android/server/pm/InstallPackageHelper.java49
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerService.java23
-rw-r--r--services/core/java/com/android/server/pm/PreferredActivityHelper.java24
-rw-r--r--services/core/java/com/android/server/pm/Settings.java46
-rw-r--r--services/core/java/com/android/server/policy/ModifierShortcutManager.java32
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java102
-rw-r--r--services/core/java/com/android/server/power/PowerManagerService.java52
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryStatsImpl.java2
-rw-r--r--services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java49
-rw-r--r--services/core/java/com/android/server/powerstats/PowerStatsLogger.java20
-rw-r--r--services/core/java/com/android/server/powerstats/PowerStatsService.java28
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java14
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java26
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java6
-rw-r--r--services/core/java/com/android/server/wm/ActivityMetricsLogger.java17
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java35
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java46
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java7
-rw-r--r--services/core/java/com/android/server/wm/Dimmer.java15
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java6
-rw-r--r--services/core/java/com/android/server/wm/DisplayPolicy.java88
-rw-r--r--services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java7
-rw-r--r--services/core/java/com/android/server/wm/InsetsSourceProvider.java38
-rw-r--r--services/core/java/com/android/server/wm/LetterboxConfiguration.java48
-rw-r--r--services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java47
-rw-r--r--services/core/java/com/android/server/wm/LetterboxUiController.java68
-rw-r--r--services/core/java/com/android/server/wm/RecentTasks.java26
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java4
-rw-r--r--services/core/java/com/android/server/wm/Task.java24
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java27
-rw-r--r--services/core/java/com/android/server/wm/Transition.java31
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java41
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java2
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerShellCommand.java12
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java13
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java8
-rw-r--r--services/core/jni/com_android_server_input_InputManagerService.cpp13
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java11
-rw-r--r--services/tests/InputMethodSystemServerTests/AndroidManifest.xml1
-rw-r--r--services/tests/InputMethodSystemServerTests/TEST_MAPPING11
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml2
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java484
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java7
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java30
-rw-r--r--services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java6
-rw-r--r--services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp2
-rw-r--r--services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml2
-rw-r--r--services/tests/displayservicetests/Android.bp19
-rw-r--r--services/tests/displayservicetests/AndroidManifest.xml9
-rw-r--r--services/tests/displayservicetests/TEST_MAPPING10
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/BrightnessSynchronizerTest.java)0
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DensityMappingTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/DensityMappingTest.java)0
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java)4
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java)6
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java)7
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java)0
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java)60
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java)0
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java)0
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java6
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/color/DisplayTransformManagerTest.java)0
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java270
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java (renamed from services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java)0
-rw-r--r--services/tests/mockingservicestests/Android.bp11
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java2
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java23
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/display/OWNERS1
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java307
-rw-r--r--services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java73
-rw-r--r--services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING18
-rw-r--r--services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionConsentManagerTest.java172
-rw-r--r--services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING18
-rw-r--r--services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java58
-rw-r--r--services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java203
-rwxr-xr-xservices/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java405
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java5
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java12
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java8
-rw-r--r--services/tests/wmtests/Android.bp1
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java245
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java49
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java20
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DimmerTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java51
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java82
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java20
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java171
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java19
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TransitionTests.java72
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java7
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java38
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java30
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java7
-rw-r--r--telephony/java/android/telephony/CarrierConfigManager.java37
-rw-r--r--telephony/java/android/telephony/NetworkRegistrationInfo.java18
-rw-r--r--telephony/java/android/telephony/SubscriptionManager.java9
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java13
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt13
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt152
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt8
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt12
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt188
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt101
-rw-r--r--tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt5
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml19
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml8
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml14
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml21
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml37
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml7
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java4
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java43
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java12
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java36
-rw-r--r--tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java (renamed from packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt)19
-rw-r--r--tests/Input/src/com/android/test/input/MotionPredictorTest.kt2
-rw-r--r--tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java32
-rw-r--r--tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java24
-rw-r--r--tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java5
-rw-r--r--tests/Internal/src/android/service/wallpaper/OWNERS4
-rw-r--r--tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java13
-rw-r--r--tests/testables/src/android/testing/TestableLooper.java42
-rw-r--r--tests/testables/tests/src/android/testing/TestableLooperTest.java40
682 files changed, 24414 insertions, 11923 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 13903acc0439..f429966e042a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -56,6 +56,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.modules.expresslog.Counter;
import com.android.server.LocalServices;
import com.android.server.job.GrantedUriPermissions;
import com.android.server.job.JobSchedulerInternal;
@@ -161,6 +162,9 @@ public final class JobStatus {
/** If the job is going to be passed an unmetered network. */
private boolean mHasAccessToUnmetered;
+ /** If the effective bucket has been downgraded once due to being buggy. */
+ private boolean mIsDowngradedDueToBuggyApp;
+
/**
* The additional set of dynamic constraints that must be met if this is an expedited job that
* had a long enough run while the device was Dozing or in battery saver.
@@ -1173,18 +1177,32 @@ public final class JobStatus {
// like other ACTIVE apps.
return ACTIVE_INDEX;
}
+
+ final int bucketWithMediaExemption;
+ if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
+ && mHasMediaBackupExemption) {
+ // Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since
+ // media backup jobs are important to the user, and the source package may not have
+ // been used directly in a while.
+ bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket);
+ } else {
+ bucketWithMediaExemption = actualBucket;
+ }
+
// If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket
// (potentially because it's used frequently by the user), limit its effective bucket
// so that it doesn't get to run as much as a normal ACTIVE app.
- final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX;
- if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
- && mHasMediaBackupExemption) {
- // Treat it as if it's at least WORKING_INDEX since media backup jobs are important
- // to the user, and the
- // source package may not have been used directly in a while.
- return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket));
+ if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) {
+ if (!mIsDowngradedDueToBuggyApp) {
+ // Safety check to avoid logging multiple times for the same job.
+ Counter.logIncrementWithUid(
+ "job_scheduler.value_job_quota_reduced_due_to_buggy_uid",
+ getTimeoutBlameUid());
+ mIsDowngradedDueToBuggyApp = true;
+ }
+ return WORKING_INDEX;
}
- return Math.max(highestBucket, actualBucket);
+ return bucketWithMediaExemption;
}
/** Returns the real standby bucket of the job. */
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index eb672dcc42fd..b159321af837 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -48,9 +48,11 @@ import android.widget.RemoteViews;
import com.android.internal.appwidget.IAppWidgetService;
import com.android.internal.os.BackgroundThread;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
/**
* Updates AppWidget state; gets information about installed AppWidget providers and other
@@ -785,7 +787,25 @@ public class AppWidgetManager {
return;
}
try {
- mService.notifyAppWidgetViewDataChanged(mPackageName, appWidgetIds, viewId);
+ if (RemoteViews.isAdapterConversionEnabled()) {
+ List<CompletableFuture<Void>> updateFutures = new ArrayList<>();
+ for (int i = 0; i < appWidgetIds.length; i++) {
+ final int widgetId = appWidgetIds[i];
+ updateFutures.add(CompletableFuture.runAsync(() -> {
+ try {
+ RemoteViews views = mService.getAppWidgetViews(mPackageName, widgetId);
+ if (views.replaceRemoteCollections(viewId)) {
+ updateAppWidget(widgetId, views);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error notifying changes in RemoteViews", e);
+ }
+ }));
+ }
+ CompletableFuture.allOf(updateFutures.toArray(CompletableFuture[]::new)).join();
+ } else {
+ mService.notifyAppWidgetViewDataChanged(mPackageName, appWidgetIds, viewId);
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl
index c9735b05cba4..86087cb0c0ef 100644
--- a/core/java/android/content/pm/IShortcutService.aidl
+++ b/core/java/android/content/pm/IShortcutService.aidl
@@ -61,7 +61,7 @@ interface IShortcutService {
void resetThrottling(); // system only API for developer opsions
- void onApplicationActive(String packageName, int userId); // system only API for sysUI
+ oneway void onApplicationActive(String packageName, int userId); // system only API for sysUI
byte[] getBackupPayload(int user);
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 66aadac6295d..9933c8be6015 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2354,6 +2354,7 @@ public abstract class PackageManager {
USER_MIN_ASPECT_RATIO_4_3,
USER_MIN_ASPECT_RATIO_16_9,
USER_MIN_ASPECT_RATIO_3_2,
+ USER_MIN_ASPECT_RATIO_FULLSCREEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface UserMinAspectRatio {}
@@ -2375,8 +2376,9 @@ public abstract class PackageManager {
/**
* Aspect ratio override code: user forces app to the aspect ratio of the device display size.
- * This will be the portrait aspect ratio of the device if the app is portrait or the landscape
- * aspect ratio of the device if the app is landscape.
+ * This will be the portrait aspect ratio of the device if the app has fixed portrait
+ * orientation or the landscape aspect ratio of the device if the app has fixed landscape
+ * orientation.
*
* @hide
*/
@@ -2400,6 +2402,12 @@ public abstract class PackageManager {
*/
public static final int USER_MIN_ASPECT_RATIO_3_2 = 5;
+ /**
+ * Aspect ratio override code: user forces app to fullscreen
+ * @hide
+ */
+ public static final int USER_MIN_ASPECT_RATIO_FULLSCREEN = 6;
+
/** @hide */
@IntDef(flag = true, prefix = { "DELETE_" }, value = {
DELETE_KEEP_DATA,
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 022f3c4c3a20..4323bf8af5fa 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1780,6 +1780,15 @@ public final class DisplayManager {
* @hide
*/
String KEY_USE_NORMAL_BRIGHTNESS_MODE_CONTROLLER = "use_normal_brightness_mode_controller";
+
+ /**
+ * Key for disabling screen wake locks while apps are in cached state.
+ * Read value via {@link android.provider.DeviceConfig#getBoolean(String, String, boolean)}
+ * with {@link android.provider.DeviceConfig#NAMESPACE_DISPLAY_MANAGER} as the namespace.
+ * @hide
+ */
+ String KEY_DISABLE_SCREEN_WAKE_LOCKS_WHILE_CACHED =
+ "disable_screen_wake_locks_while_cached";
}
/**
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index 70b72c809524..b99996ff83c8 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -433,7 +433,7 @@ class IInputMethodWrapper extends IInputMethod.Stub
@BinderThread
@Override
public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
- int flags, ResultReceiver resultReceiver) {
+ @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
flags, showInputToken, resultReceiver, statsToken));
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index e472a40617ee..60b11b425184 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -606,6 +606,7 @@ public class InputMethodService extends AbstractInputMethodService {
InputConnection mStartedInputConnection;
EditorInfo mInputEditorInfo;
+ @InputMethod.ShowFlags
int mShowInputFlags;
boolean mShowInputRequested;
boolean mLastShowInputRequested;
@@ -930,8 +931,9 @@ public class InputMethodService extends AbstractInputMethodService {
*/
@MainThread
@Override
- public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
- IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
+ public void showSoftInputWithToken(@InputMethod.ShowFlags int flags,
+ ResultReceiver resultReceiver, IBinder showInputToken,
+ @Nullable ImeTracker.Token statsToken) {
mSystemCallingShowSoftInput = true;
mCurShowInputToken = showInputToken;
mCurStatsToken = statsToken;
@@ -949,7 +951,7 @@ public class InputMethodService extends AbstractInputMethodService {
*/
@MainThread
@Override
- public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+ public void showSoftInput(@InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
ImeTracker.forLogging().onProgress(
mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
if (DEBUG) Log.v(TAG, "showSoftInput()");
@@ -1325,7 +1327,8 @@ public class InputMethodService extends AbstractInputMethodService {
* InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
*/
@Deprecated
- public void toggleSoftInput(int showFlags, int hideFlags) {
+ public void toggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+ @InputMethodManager.HideFlags int hideFlags) {
InputMethodService.this.onToggleSoftInput(showFlags, hideFlags);
}
@@ -2797,18 +2800,16 @@ public class InputMethodService extends AbstractInputMethodService {
* {@link #onEvaluateInputViewShown()}, {@link #onEvaluateFullscreenMode()},
* and the current configuration to decide whether the input view should
* be shown at this point.
- *
- * @param flags Provides additional information about the show request,
- * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}.
+ *
* @param configChange This is true if we are re-showing due to a
* configuration change.
* @return Returns true to indicate that the window should be shown.
*/
- public boolean onShowInputRequested(int flags, boolean configChange) {
+ public boolean onShowInputRequested(@InputMethod.ShowFlags int flags, boolean configChange) {
if (!onEvaluateInputViewShown()) {
return false;
}
- if ((flags&InputMethod.SHOW_EXPLICIT) == 0) {
+ if ((flags & InputMethod.SHOW_EXPLICIT) == 0) {
if (!configChange && onEvaluateFullscreenMode() && !isInputViewShown()) {
// Don't show if this is not explicitly requested by the user and
// the input method is fullscreen unless it is already shown. That
@@ -2834,14 +2835,14 @@ public class InputMethodService extends AbstractInputMethodService {
* exposed to IME authors as an overridable public method without {@code @CallSuper}, we have
* to have this method to ensure that those internal states are always updated no matter how
* {@link #onShowInputRequested(int, boolean)} is overridden by the IME author.
- * @param flags Provides additional information about the show request,
- * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}.
+ *
* @param configChange This is true if we are re-showing due to a
* configuration change.
* @return Returns true to indicate that the window should be shown.
* @see #onShowInputRequested(int, boolean)
*/
- private boolean dispatchOnShowInputRequested(int flags, boolean configChange) {
+ private boolean dispatchOnShowInputRequested(@InputMethod.ShowFlags int flags,
+ boolean configChange) {
final boolean result = onShowInputRequested(flags, configChange);
mInlineSuggestionSessionController.notifyOnShowInputRequested(result);
if (result) {
@@ -2985,8 +2986,6 @@ public class InputMethodService extends AbstractInputMethodService {
ImeTracing.getInstance().triggerServiceDump(
"InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
null /* icProto */);
- ImeTracker.forLogging().onProgress(mCurStatsToken,
- ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
mPrivOps.applyImeVisibilityAsync(setVisible
? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
}
@@ -3274,16 +3273,13 @@ public class InputMethodService extends AbstractInputMethodService {
*
* The input method will continue running, but the user can no longer use it to generate input
* by touching the screen.
- *
- * @see InputMethodManager#HIDE_IMPLICIT_ONLY
- * @see InputMethodManager#HIDE_NOT_ALWAYS
- * @param flags Provides additional operating flags.
*/
- public void requestHideSelf(int flags) {
+ public void requestHideSelf(@InputMethodManager.HideFlags int flags) {
requestHideSelf(flags, SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_IME);
}
- private void requestHideSelf(int flags, @SoftInputShowHideReason int reason) {
+ private void requestHideSelf(@InputMethodManager.HideFlags int flags,
+ @SoftInputShowHideReason int reason) {
ImeTracing.getInstance().triggerServiceDump("InputMethodService#requestHideSelf", mDumper,
null /* icProto */);
mPrivOps.hideMySoftInput(flags, reason);
@@ -3292,12 +3288,8 @@ public class InputMethodService extends AbstractInputMethodService {
/**
* Show the input method's soft input area, so the user sees the input method window and can
* interact with it.
- *
- * @see InputMethodManager#SHOW_IMPLICIT
- * @see InputMethodManager#SHOW_FORCED
- * @param flags Provides additional operating flags.
*/
- public final void requestShowSelf(int flags) {
+ public final void requestShowSelf(@InputMethodManager.ShowFlags int flags) {
ImeTracing.getInstance().triggerServiceDump("InputMethodService#requestShowSelf", mDumper,
null /* icProto */);
mPrivOps.showMySoftInput(flags);
@@ -3457,7 +3449,8 @@ public class InputMethodService extends AbstractInputMethodService {
/**
* Handle a request by the system to toggle the soft input area.
*/
- private void onToggleSoftInput(int showFlags, int hideFlags) {
+ private void onToggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+ @InputMethodManager.HideFlags int hideFlags) {
if (DEBUG) Log.v(TAG, "toggleSoftInput()");
if (isInputViewShown()) {
requestHideSelf(
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index ac6b2b23cdf5..3c5757dd5615 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -1985,13 +1985,14 @@ public class CallLog {
Log.w(LOG_TAG, "Failed to insert into call log; null result uri.");
}
+ int numDeleted;
if (values.containsKey(PHONE_ACCOUNT_ID)
&& !TextUtils.isEmpty(values.getAsString(PHONE_ACCOUNT_ID))
&& values.containsKey(PHONE_ACCOUNT_COMPONENT_NAME)
&& !TextUtils.isEmpty(values.getAsString(PHONE_ACCOUNT_COMPONENT_NAME))) {
// Only purge entries for the same phone account.
- resolver.delete(uri, "_id IN " +
- "(SELECT _id FROM calls"
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls"
+ " WHERE " + PHONE_ACCOUNT_COMPONENT_NAME + " = ?"
+ " AND " + PHONE_ACCOUNT_ID + " = ?"
+ " ORDER BY " + DEFAULT_SORT_ORDER
@@ -2001,14 +2002,15 @@ public class CallLog {
});
} else {
// No valid phone account specified, so default to the old behavior.
- resolver.delete(uri, "_id IN " +
- "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ numDeleted = resolver.delete(uri, "_id IN "
+ + "(SELECT _id FROM calls ORDER BY " + DEFAULT_SORT_ORDER
+ " LIMIT -1 OFFSET 500)", null);
}
+ Log.i(LOG_TAG, "addEntry: cleaned up " + numDeleted + " old entries");
return result;
} catch (IllegalArgumentException e) {
- Log.w(LOG_TAG, "Failed to insert calllog", e);
+ Log.e(LOG_TAG, "Failed to insert calllog", e);
// Even though we make sure the target user is running and decrypted before calling
// this method, there's a chance that the user just got shut down, in which case
// we'll still get "IllegalArgumentException: Unknown URL content://call_log/calls".
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index dbc1be141571..d9ac4850e924 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -868,6 +868,11 @@ public abstract class WallpaperService extends Service {
* This will trigger a {@link #onComputeColors()} call.
*/
public void notifyColorsChanged() {
+ if (mDestroyed) {
+ Log.i(TAG, "Ignoring notifyColorsChanged(), Engine has already been destroyed.");
+ return;
+ }
+
final long now = mClockFunction.get();
if (now - mLastColorInvalidation < NOTIFY_COLORS_RATE_LIMIT_MS) {
Log.w(TAG, "This call has been deferred. You should only call "
@@ -2226,7 +2231,11 @@ public abstract class WallpaperService extends Service {
}
}
- void detach() {
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public void detach() {
if (mDestroyed) {
return;
}
@@ -2442,6 +2451,14 @@ public abstract class WallpaperService extends Service {
}
public void reportShown() {
+ if (mEngine == null) {
+ Log.i(TAG, "Can't report null engine as shown.");
+ return;
+ }
+ if (mEngine.mDestroyed) {
+ Log.i(TAG, "Engine was destroyed before we could draw.");
+ return;
+ }
if (!mShownReported) {
mShownReported = true;
Trace.beginSection("WPMS.mConnection.engineShown");
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9d2bb58cdf0f..2f12fecb7fd0 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -311,6 +311,16 @@ public final class ViewRootImpl implements ViewParent,
SystemProperties.getBoolean("persist.wm.debug.client_transient", false);
/**
+ * Whether the client (system UI) is handling the immersive confirmation window. If
+ * {@link CLIENT_TRANSIENT} is set to true, the immersive confirmation window will always be the
+ * client instance and this flag will be ignored. Otherwise, the immersive confirmation window
+ * can be switched freely by this flag.
+ * @hide
+ */
+ public static final boolean CLIENT_IMMERSIVE_CONFIRMATION =
+ SystemProperties.getBoolean("persist.wm.debug.client_immersive_confirmation", false);
+
+ /**
* Whether the client should compute the window frame on its own.
* @hide
*/
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index d5f2aa3b3631..65677cd7c3fd 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3099,6 +3099,16 @@ public interface WindowManager extends ViewManager {
public static final int PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE = 1 << 16;
/**
+ * Flag to indicate that this window is a immersive mode confirmation window. The window
+ * should be ignored when calculating insets control. This is used for prompt window
+ * triggered by insets visibility changes. If it can take over the insets control, the
+ * visibility will change unexpectedly and the window may dismiss itself. Power button panic
+ * handling will be disabled when this window exists.
+ * @hide
+ */
+ public static final int PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW = 1 << 17;
+
+ /**
* Flag to indicate that any window added by an application process that is of type
* {@link #TYPE_TOAST} or that requires
* {@link android.app.AppOpsManager#OP_SYSTEM_ALERT_WINDOW} permission should be hidden when
@@ -3242,6 +3252,7 @@ public interface WindowManager extends ViewManager {
PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME,
PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS,
PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
+ PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
@@ -3326,6 +3337,10 @@ public interface WindowManager extends ViewManager {
equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE,
name = "SUSTAINED_PERFORMANCE_MODE"),
@ViewDebug.FlagToString(
+ mask = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
+ equals = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW,
+ name = "IMMERSIVE_CONFIRMATION_WINDOW"),
+ @ViewDebug.FlagToString(
mask = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
equals = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"),
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index ce2c18080b91..467daa028afd 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -295,8 +295,8 @@ final class IInputMethodManagerGlobalInvoker {
@AnyThread
static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
- @Nullable ResultReceiver resultReceiver,
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+ int lastClickToolType, @Nullable ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
@@ -312,7 +312,7 @@ final class IInputMethodManagerGlobalInvoker {
@AnyThread
static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags,
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
@Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final IInputMethodManager service = getService();
if (service == null) {
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 92380ed7a7bc..9340f46b257f 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -17,6 +17,7 @@
package android.view.inputmethod;
import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -36,6 +37,8 @@ import com.android.internal.inputmethod.IInputMethod;
import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -269,6 +272,14 @@ public interface InputMethod {
*/
@MainThread
public void revokeSession(InputMethodSession session);
+
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SHOW_" }, value = {
+ SHOW_EXPLICIT,
+ SHOW_FORCED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ShowFlags {}
/**
* Flag for {@link #showSoftInput}: this show has been explicitly
@@ -288,8 +299,6 @@ public interface InputMethod {
/**
* Request that any soft input part of the input method be shown to the user.
*
- * @param flags Provides additional information about the show request.
- * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
* The result code should be
@@ -304,7 +313,7 @@ public interface InputMethod {
* @hide
*/
@MainThread
- public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+ public default void showSoftInputWithToken(@ShowFlags int flags, ResultReceiver resultReceiver,
IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
showSoftInput(flags, resultReceiver);
}
@@ -312,8 +321,6 @@ public interface InputMethod {
/**
* Request that any soft input part of the input method be shown to the user.
*
- * @param flags Provides additional information about the show request.
- * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
* The result code should be
@@ -323,11 +330,12 @@ public interface InputMethod {
* {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
*/
@MainThread
- public void showSoftInput(int flags, ResultReceiver resultReceiver);
+ public void showSoftInput(@ShowFlags int flags, ResultReceiver resultReceiver);
/**
* Request that any soft input part of the input method be hidden from the user.
- * @param flags Provides additional information about the show request.
+ *
+ * @param flags Provides additional information about the hide request.
* Currently always 0.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
@@ -350,7 +358,8 @@ public interface InputMethod {
/**
* Request that any soft input part of the input method be hidden from the user.
- * @param flags Provides additional information about the show request.
+ *
+ * @param flags Provides additional information about the hide request.
* Currently always 0.
* @param resultReceiver The client requesting the show may wish to
* be told the impact of their request, which should be supplied here.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index b3233141f126..df9c26834fd4 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -39,6 +39,7 @@ import android.Manifest;
import android.annotation.DisplayContext;
import android.annotation.DrawableRes;
import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresFeature;
@@ -122,6 +123,8 @@ import com.android.internal.view.IInputMethodManager;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
@@ -2034,6 +2037,14 @@ public final class InputMethodManager {
}
}
+ /** @hide */
+ @IntDef(flag = true, prefix = { "SHOW_" }, value = {
+ SHOW_IMPLICIT,
+ SHOW_FORCED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShowFlags {}
+
/**
* Flag for {@link #showSoftInput} to indicate that this is an implicit
* request to show the input window, not as the result of a direct request
@@ -2065,10 +2076,8 @@ public final class InputMethodManager {
* {@link View#isFocused view focus}, and its containing window has
* {@link View#hasWindowFocus window focus}. Otherwise the call fails and
* returns {@code false}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #SHOW_IMPLICIT} bit set.
*/
- public boolean showSoftInput(View view, int flags) {
+ public boolean showSoftInput(View view, @ShowFlags int flags) {
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
if (fallbackImm != null) {
@@ -2131,21 +2140,20 @@ public final class InputMethodManager {
* {@link View#isFocused view focus}, and its containing window has
* {@link View#hasWindowFocus window focus}. Otherwise the call fails and
* returns {@code false}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #SHOW_IMPLICIT} bit set.
* @param resultReceiver If non-null, this will be called by the IME when
* it has processed your request to tell you what it has done. The result
* code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
* {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
* {@link #RESULT_HIDDEN}.
*/
- public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
+ public boolean showSoftInput(View view, @ShowFlags int flags, ResultReceiver resultReceiver) {
return showSoftInput(view, null /* statsToken */, flags, resultReceiver,
SoftInputShowHideReason.SHOW_SOFT_INPUT);
}
- private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken, int flags,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken,
+ @ShowFlags int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
if (statsToken == null) {
statsToken = ImeTracker.forLogging().onRequestShow(null /* component */,
Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason);
@@ -2199,7 +2207,7 @@ public final class InputMethodManager {
*/
@Deprecated
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
- public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
+ public void showSoftInputUnchecked(@ShowFlags int flags, ResultReceiver resultReceiver) {
synchronized (mH) {
final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestShow(
null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
@@ -2230,6 +2238,14 @@ public final class InputMethodManager {
}
}
+ /** @hide */
+ @IntDef(flag = true, prefix = { "HIDE_" }, value = {
+ HIDE_IMPLICIT_ONLY,
+ HIDE_NOT_ALWAYS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface HideFlags {}
+
/**
* Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)}
* to indicate that the soft input window should only be hidden if it was not explicitly shown
@@ -2251,10 +2267,8 @@ public final class InputMethodManager {
*
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
*/
- public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
+ public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags) {
return hideSoftInputFromWindow(windowToken, flags, null);
}
@@ -2276,21 +2290,19 @@ public final class InputMethodManager {
*
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
* @param resultReceiver If non-null, this will be called by the IME when
* it has processed your request to tell you what it has done. The result
* code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
* {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
* {@link #RESULT_HIDDEN}.
*/
- public boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+ public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
ResultReceiver resultReceiver) {
return hideSoftInputFromWindow(windowToken, flags, resultReceiver,
SoftInputShowHideReason.HIDE_SOFT_INPUT);
}
- private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+ private boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide(
null /* component */, Process.myUid(),
@@ -2493,12 +2505,6 @@ public final class InputMethodManager {
* If not the input window will be displayed.
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
- * @param showFlags Provides additional operating flags. May be
- * 0 or have the {@link #SHOW_IMPLICIT},
- * {@link #SHOW_FORCED} bit set.
- * @param hideFlags Provides additional operating flags. May be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
- * {@link #HIDE_NOT_ALWAYS} bit set.
*
* @deprecated Use {@link #showSoftInput(View, int)} or
* {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
@@ -2507,7 +2513,8 @@ public final class InputMethodManager {
* has an effect if the calling app is the current IME focus.
*/
@Deprecated
- public void toggleSoftInputFromWindow(IBinder windowToken, int showFlags, int hideFlags) {
+ public void toggleSoftInputFromWindow(IBinder windowToken, @ShowFlags int showFlags,
+ @HideFlags int hideFlags) {
ImeTracing.getInstance().triggerClientDump(
"InputMethodManager#toggleSoftInputFromWindow", InputMethodManager.this,
null /* icProto */);
@@ -2525,12 +2532,6 @@ public final class InputMethodManager {
*
* If the input window is already displayed, it gets hidden.
* If not the input window will be displayed.
- * @param showFlags Provides additional operating flags. May be
- * 0 or have the {@link #SHOW_IMPLICIT},
- * {@link #SHOW_FORCED} bit set.
- * @param hideFlags Provides additional operating flags. May be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
- * {@link #HIDE_NOT_ALWAYS} bit set.
*
* @deprecated Use {@link #showSoftInput(View, int)} or
* {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
@@ -2539,7 +2540,7 @@ public final class InputMethodManager {
* has an effect if the calling app is the current IME focus.
*/
@Deprecated
- public void toggleSoftInput(int showFlags, int hideFlags) {
+ public void toggleSoftInput(@ShowFlags int showFlags, @HideFlags int hideFlags) {
ImeTracing.getInstance().triggerClientDump(
"InputMethodManager#toggleSoftInput", InputMethodManager.this,
null /* icProto */);
@@ -3552,15 +3553,12 @@ public final class InputMethodManager {
* @param token Supplies the identifying token given to an input method
* when it was started, which allows it to perform this operation on
* itself.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
- * {@link #HIDE_NOT_ALWAYS} bit set.
* @deprecated Use {@link InputMethodService#requestHideSelf(int)} instead. This method was
* intended for IME developers who should be accessing APIs through the service. APIs in this
* class are intended for app developers interacting with the IME.
*/
@Deprecated
- public void hideSoftInputFromInputMethod(IBinder token, int flags) {
+ public void hideSoftInputFromInputMethod(IBinder token, @HideFlags int flags) {
InputMethodPrivilegedOperationsRegistry.get(token).hideMySoftInput(
flags, SoftInputShowHideReason.HIDE_SOFT_INPUT_IMM_DEPRECATION);
}
@@ -3574,15 +3572,12 @@ public final class InputMethodManager {
* @param token Supplies the identifying token given to an input method
* when it was started, which allows it to perform this operation on
* itself.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link #SHOW_IMPLICIT} or
- * {@link #SHOW_FORCED} bit set.
* @deprecated Use {@link InputMethodService#requestShowSelf(int)} instead. This method was
* intended for IME developers who should be accessing APIs through the service. APIs in this
* class are intended for app developers interacting with the IME.
*/
@Deprecated
- public void showSoftInputFromInputMethod(IBinder token, int flags) {
+ public void showSoftInputFromInputMethod(IBinder token, @ShowFlags int flags) {
InputMethodPrivilegedOperationsRegistry.get(token).showMySoftInput(flags);
}
diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java
index af6af14472db..4f48cb684e8c 100644
--- a/core/java/android/view/inputmethod/InputMethodSession.java
+++ b/core/java/android/view/inputmethod/InputMethodSession.java
@@ -169,12 +169,6 @@ public interface InputMethodSession {
/**
* Toggle the soft input window.
* Applications can toggle the state of the soft input window.
- * @param showFlags Provides additional operating flags. May be
- * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT},
- * {@link InputMethodManager#SHOW_FORCED} bit set.
- * @param hideFlags Provides additional operating flags. May be
- * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY},
- * {@link InputMethodManager#HIDE_NOT_ALWAYS} bit set.
*
* @deprecated Starting in {@link android.os.Build.VERSION_CODES#S} the system no longer invokes
* this method, instead it explicitly shows or hides the IME. An {@code InputMethodService}
@@ -182,7 +176,8 @@ public interface InputMethodSession {
* InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
*/
@Deprecated
- public void toggleSoftInput(int showFlags, int hideFlags);
+ public void toggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+ @InputMethodManager.HideFlags int hideFlags);
/**
* This method is called when the cursor and/or the character position relevant to text input
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index d9e76fefad7f..a2f95fa9df45 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -786,6 +786,42 @@ public class RemoteViews implements Parcelable, Filter {
}
}
+ /**
+ * @hide
+ * @return True if there is a change
+ */
+ public boolean replaceRemoteCollections(int viewId) {
+ boolean isActionReplaced = false;
+ if (mActions != null) {
+ for (int i = 0; i < mActions.size(); i++) {
+ Action action = mActions.get(i);
+ if (action instanceof SetRemoteCollectionItemListAdapterAction itemsAction
+ && itemsAction.viewId == viewId
+ && itemsAction.mServiceIntent != null) {
+ mActions.set(i, new SetRemoteCollectionItemListAdapterAction(itemsAction.viewId,
+ itemsAction.mServiceIntent));
+ isActionReplaced = true;
+ } else if (action instanceof ViewGroupActionAdd groupAction
+ && groupAction.mNestedViews != null) {
+ isActionReplaced |= groupAction.mNestedViews.replaceRemoteCollections(viewId);
+ }
+ }
+ }
+ if (mSizedRemoteViews != null) {
+ for (int i = 0; i < mSizedRemoteViews.size(); i++) {
+ isActionReplaced |= mSizedRemoteViews.get(i).replaceRemoteCollections(viewId);
+ }
+ }
+ if (mLandscape != null) {
+ isActionReplaced |= mLandscape.replaceRemoteCollections(viewId);
+ }
+ if (mPortrait != null) {
+ isActionReplaced |= mPortrait.replaceRemoteCollections(viewId);
+ }
+
+ return isActionReplaced;
+ }
+
private static void visitIconUri(Icon icon, @NonNull Consumer<Uri> visitor) {
if (icon != null && (icon.getType() == Icon.TYPE_URI
|| icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) {
@@ -1059,18 +1095,21 @@ public class RemoteViews implements Parcelable, Filter {
private class SetRemoteCollectionItemListAdapterAction extends Action {
private @NonNull CompletableFuture<RemoteCollectionItems> mItemsFuture;
+ final Intent mServiceIntent;
SetRemoteCollectionItemListAdapterAction(@IdRes int id,
@NonNull RemoteCollectionItems items) {
viewId = id;
items.setHierarchyRootData(getHierarchyRootData());
mItemsFuture = CompletableFuture.completedFuture(items);
+ mServiceIntent = null;
}
SetRemoteCollectionItemListAdapterAction(@IdRes int id, Intent intent) {
viewId = id;
mItemsFuture = getItemsFutureFromIntentWithTimeout(intent);
setHierarchyRootData(getHierarchyRootData());
+ mServiceIntent = intent;
}
private static CompletableFuture<RemoteCollectionItems> getItemsFutureFromIntentWithTimeout(
@@ -1119,6 +1158,7 @@ public class RemoteViews implements Parcelable, Filter {
viewId = parcel.readInt();
mItemsFuture = CompletableFuture.completedFuture(
new RemoteCollectionItems(parcel, getHierarchyRootData()));
+ mServiceIntent = parcel.readTypedObject(Intent.CREATOR);
}
@Override
@@ -1148,6 +1188,7 @@ public class RemoteViews implements Parcelable, Filter {
dest.writeInt(viewId);
RemoteCollectionItems items = getCollectionItemsFromFuture(mItemsFuture);
items.writeToParcel(dest, flags, /* attached= */ true);
+ dest.writeTypedObject(mServiceIntent, flags);
}
@Override
@@ -4765,9 +4806,7 @@ public class RemoteViews implements Parcelable, Filter {
* providing data to the RemoteViewsAdapter
*/
public void setRemoteAdapter(@IdRes int viewId, Intent intent) {
- if (AppGlobals.getIntCoreSetting(
- SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION,
- SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT ? 1 : 0) == 1) {
+ if (isAdapterConversionEnabled()) {
addAction(new SetRemoteCollectionItemListAdapterAction(viewId, intent));
return;
}
@@ -4775,6 +4814,16 @@ public class RemoteViews implements Parcelable, Filter {
}
/**
+ * @hide
+ * @return True if the remote adapter conversion is enabled
+ */
+ public static boolean isAdapterConversionEnabled() {
+ return AppGlobals.getIntCoreSetting(
+ SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION,
+ SystemUiDeviceConfigFlags.REMOTEVIEWS_ADAPTER_CONVERSION_DEFAULT ? 1 : 0) == 1;
+ }
+
+ /**
* Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
* ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
* This is a simpler but less flexible approach to populating collection widgets. Its use is
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index e153bb70a7ca..43fa0be6c1b7 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -80,6 +80,14 @@ public final class TaskFragmentOperation implements Parcelable {
*/
public static final int OP_TYPE_REORDER_TO_FRONT = 10;
+ /**
+ * Sets the activity navigation to be isolated, where the activity navigation on the
+ * TaskFragment is separated from the rest activities in the Task. Activities cannot be
+ * started on an isolated TaskFragment unless the activities are launched from the same
+ * TaskFragment or explicitly requested to.
+ */
+ public static final int OP_TYPE_SET_ISOLATED_NAVIGATION = 11;
+
@IntDef(prefix = { "OP_TYPE_" }, value = {
OP_TYPE_UNKNOWN,
OP_TYPE_CREATE_TASK_FRAGMENT,
@@ -92,7 +100,8 @@ public final class TaskFragmentOperation implements Parcelable {
OP_TYPE_SET_COMPANION_TASK_FRAGMENT,
OP_TYPE_SET_ANIMATION_PARAMS,
OP_TYPE_SET_RELATIVE_BOUNDS,
- OP_TYPE_REORDER_TO_FRONT
+ OP_TYPE_REORDER_TO_FRONT,
+ OP_TYPE_SET_ISOLATED_NAVIGATION
})
@Retention(RetentionPolicy.SOURCE)
public @interface OperationType {}
@@ -118,11 +127,14 @@ public final class TaskFragmentOperation implements Parcelable {
@Nullable
private final TaskFragmentAnimationParams mAnimationParams;
+ private final boolean mIsolatedNav;
+
private TaskFragmentOperation(@OperationType int opType,
@Nullable TaskFragmentCreationParams taskFragmentCreationParams,
@Nullable IBinder activityToken, @Nullable Intent activityIntent,
@Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken,
- @Nullable TaskFragmentAnimationParams animationParams) {
+ @Nullable TaskFragmentAnimationParams animationParams,
+ boolean isolatedNav) {
mOpType = opType;
mTaskFragmentCreationParams = taskFragmentCreationParams;
mActivityToken = activityToken;
@@ -130,6 +142,7 @@ public final class TaskFragmentOperation implements Parcelable {
mBundle = bundle;
mSecondaryFragmentToken = secondaryFragmentToken;
mAnimationParams = animationParams;
+ mIsolatedNav = isolatedNav;
}
private TaskFragmentOperation(Parcel in) {
@@ -140,6 +153,7 @@ public final class TaskFragmentOperation implements Parcelable {
mBundle = in.readBundle(getClass().getClassLoader());
mSecondaryFragmentToken = in.readStrongBinder();
mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
+ mIsolatedNav = in.readBoolean();
}
@Override
@@ -151,6 +165,7 @@ public final class TaskFragmentOperation implements Parcelable {
dest.writeBundle(mBundle);
dest.writeStrongBinder(mSecondaryFragmentToken);
dest.writeTypedObject(mAnimationParams, flags);
+ dest.writeBoolean(mIsolatedNav);
}
@NonNull
@@ -223,6 +238,14 @@ public final class TaskFragmentOperation implements Parcelable {
return mAnimationParams;
}
+ /**
+ * Returns whether the activity navigation on this TaskFragment is isolated. This is only
+ * useful when the op type is {@link OP_TYPE_SET_ISOLATED_NAVIGATION}.
+ */
+ public boolean isIsolatedNav() {
+ return mIsolatedNav;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
@@ -245,6 +268,7 @@ public final class TaskFragmentOperation implements Parcelable {
if (mAnimationParams != null) {
sb.append(", animationParams=").append(mAnimationParams);
}
+ sb.append(", isolatedNav=").append(mIsolatedNav);
sb.append('}');
return sb.toString();
@@ -253,7 +277,7 @@ public final class TaskFragmentOperation implements Parcelable {
@Override
public int hashCode() {
return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
- mBundle, mSecondaryFragmentToken, mAnimationParams);
+ mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav);
}
@Override
@@ -268,7 +292,8 @@ public final class TaskFragmentOperation implements Parcelable {
&& Objects.equals(mActivityIntent, other.mActivityIntent)
&& Objects.equals(mBundle, other.mBundle)
&& Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken)
- && Objects.equals(mAnimationParams, other.mAnimationParams);
+ && Objects.equals(mAnimationParams, other.mAnimationParams)
+ && mIsolatedNav == other.mIsolatedNav;
}
@Override
@@ -300,6 +325,8 @@ public final class TaskFragmentOperation implements Parcelable {
@Nullable
private TaskFragmentAnimationParams mAnimationParams;
+ private boolean mIsolatedNav;
+
/**
* @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
*/
@@ -363,12 +390,22 @@ public final class TaskFragmentOperation implements Parcelable {
}
/**
+ * Sets the activity navigation of this TaskFragment to be isolated.
+ */
+ @NonNull
+ public Builder setIsolatedNav(boolean isolatedNav) {
+ mIsolatedNav = isolatedNav;
+ return this;
+ }
+
+ /**
* Constructs the {@link TaskFragmentOperation}.
*/
@NonNull
public TaskFragmentOperation build() {
return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken,
- mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams);
+ mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams,
+ mIsolatedNav);
}
}
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 50f393b53277..2445daf89b64 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -31,6 +31,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERS
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.PermissionChecker.PID_UNKNOWN;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
@@ -356,6 +357,12 @@ public class ResolverActivity extends Activity implements
// flag set, we are now losing it. That should be a very rare case
// and we can live with this.
intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
+ intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ }
return intent;
}
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
index 81cd28023777..10336bd36c28 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -66,10 +66,6 @@ public class SystemUiSystemPropertiesFlags {
public static final Flag SHOW_STICKY_HUN_FOR_DENIED_FSI =
releasedFlag("persist.sysui.notification.show_sticky_hun_for_denied_fsi");
- /** Gating the ability for users to dismiss ongoing event notifications */
- public static final Flag ALLOW_DISMISS_ONGOING =
- releasedFlag("persist.sysui.notification.ongoing_dismissal");
-
/** Gating the redaction of OTP notifications on the lockscreen */
public static final Flag OTP_REDACTION =
devFlag("persist.sysui.notification.otp_redaction");
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 66e3333acf7c..30ebbe2bb111 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -26,6 +26,7 @@ import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
@@ -253,13 +254,11 @@ public final class InputMethodPrivilegedOperations {
/**
* Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
*
- * @param flags additional operating flags
* @param reason the reason to hide soft input
- * @see android.view.inputmethod.InputMethodManager#HIDE_IMPLICIT_ONLY
- * @see android.view.inputmethod.InputMethodManager#HIDE_NOT_ALWAYS
*/
@AnyThread
- public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason) {
+ public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
+ @SoftInputShowHideReason int reason) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
return;
@@ -275,13 +274,9 @@ public final class InputMethodPrivilegedOperations {
/**
* Calls {@link IInputMethodPrivilegedOperations#showMySoftInput(int, AndroidFuture)}
- *
- * @param flags additional operating flags
- * @see android.view.inputmethod.InputMethodManager#SHOW_IMPLICIT
- * @see android.view.inputmethod.InputMethodManager#SHOW_FORCED
*/
@AnyThread
- public void showMySoftInput(int flags) {
+ public void showMySoftInput(@InputMethodManager.ShowFlags int flags) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
return;
@@ -391,8 +386,12 @@ public final class InputMethodPrivilegedOperations {
@Nullable ImeTracker.Token statsToken) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
return;
}
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
try {
ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
} catch (RemoteException e) {
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index d2564fb9c268..c6f5086b8346 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -63,6 +63,19 @@ oneway interface IStatusBar
void cancelPreloadRecentApps();
void showScreenPinningRequest(int taskId);
+ /**
+ * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed
+ * status should be saved without user clicking on the button. This could happen when a user
+ * swipe on the edge with the confirmation prompt showing.
+ */
+ void confirmImmersivePrompt();
+
+ /**
+ * Notify system UI the immersive mode changed. This shall be removed when client immersive is
+ * enabled.
+ */
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index e0183931777b..1b1efeed5ff2 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1035,7 +1035,7 @@ public class LockPatternUtils {
CREDENTIAL_TYPE_API, CREDENTIAL_TYPE_API, mCredentialTypeQuery);
/**
- * Invalidate the credential cache
+ * Invalidate the credential type cache
* @hide
*/
public final static void invalidateCredentialTypeCache() {
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 66ff01e6e7b9..8a797c7de0a3 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2613,6 +2613,17 @@
assistant activities (ACTIVITY_TYPE_ASSISTANT) -->
<bool name="config_dismissDreamOnActivityStart">false</bool>
+ <!-- Whether to send a user activity event to PowerManager when a dream quits unexpectedly so
+ that the screen won't immediately shut off.
+
+ When a dream stops unexpectedly, such as due to an app update, if the device has been
+ inactive less than the user's screen timeout, the device goes to keyguard and times out
+ back to dreaming after a few seconds. If the device has been inactive longer, the screen
+ will immediately turn off. With this flag on, the device will go back to keyguard in all
+ scenarios rather than turning off, which gives the device a chance to start dreaming
+ again. -->
+ <bool name="config_resetScreenTimeoutOnUnexpectedDreamExit">false</bool>
+
<!-- The prefixes of dream component names that are loggable.
Matched against ComponentName#flattenToString() for dream components.
If empty, logs "other" for all. -->
@@ -5580,7 +5591,7 @@
the Option 3 is selected for R.integer.config_letterboxBackgroundType.
Values < 0 or >= 1 are ignored and 0.0 (transparent) is used instead. -->
<item name="config_letterboxBackgroundWallaperDarkScrimAlpha" format="float" type="dimen">
- 0.68
+ 0.75
</item>
<!-- Corners appearance of the letterbox background.
@@ -5605,7 +5616,7 @@
but isn't supported on the device or both dark scrim alpha and blur radius aren't
provided.
-->
- <color name="config_letterboxBackgroundColor">@color/system_on_secondary_fixed</color>
+ <color name="config_letterboxBackgroundColor">@color/system_neutral1_1000</color>
<!-- Horizontal position of a center of the letterboxed app window.
0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
@@ -5707,6 +5718,9 @@
<!-- Whether per-app user aspect ratio override settings is enabled -->
<bool name="config_appCompatUserAppAspectRatioSettingsIsEnabled">false</bool>
+ <!-- Whether per-app fullscreen override option is allowed in user aspect ratio settings -->
+ <bool name="config_appCompatUserAppAspectRatioFullscreenIsEnabled">false</bool>
+
<!-- Whether sending compat fake focus for split screen resumed activities is enabled.
Needed because some game engines wait to get focus before drawing the content of
the app which isn't guaranteed by default in multi-window modes. -->
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 08c40ba0e823..b7a5bc826641 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -151,6 +151,23 @@
<integer name="config_timeout_to_receive_delivered_ack_millis">300000</integer>
<java-symbol type="integer" name="config_timeout_to_receive_delivered_ack_millis" />
+ <!-- Telephony config for services supported by satellite providers. The format of each config
+ string in the array is as follows: "PLMN_1:service_1,service_2,..."
+ where PLMN is the satellite PLMN of a provider and service is an integer with the
+ following value:
+ 1 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_VOICE}
+ 2 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_DATA}
+ 3 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_SMS}
+ 4 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_VIDEO}
+ 5 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_EMERGENCY}
+ Example of a config string: "10011:2,3"
+
+ The PLMNs not configured in this array will be ignored and will not be used for satellite
+ scanning. -->
+ <string-array name="config_satellite_services_supported_by_providers" translatable="false">
+ </string-array>
+ <java-symbol type="array" name="config_satellite_services_supported_by_providers" />
+
<!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks
will not perform handover if the target transport is out of service, or VoPS not
supported. The network will be torn down on the source transport, and will be
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bffcf5f27ade..0fe560eb51a6 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2211,6 +2211,7 @@
<java-symbol type="array" name="config_supportedDreamComplications" />
<java-symbol type="array" name="config_disabledDreamComponents" />
<java-symbol type="bool" name="config_dismissDreamOnActivityStart" />
+ <java-symbol type="bool" name="config_resetScreenTimeoutOnUnexpectedDreamExit" />
<java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" />
<java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" />
<java-symbol type="integer" name="config_minDreamOverlayDurationMs" />
@@ -4535,6 +4536,7 @@
<!-- Whether per-app user aspect ratio override settings is enabled -->
<java-symbol type="bool" name="config_appCompatUserAppAspectRatioSettingsIsEnabled" />
+ <java-symbol type="bool" name="config_appCompatUserAppAspectRatioFullscreenIsEnabled" />
<java-symbol type="bool" name="config_isCompatFakeFocusEnabled" />
<java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
diff --git a/core/tests/coretests/src/android/content/TEST_MAPPING b/core/tests/coretests/src/android/content/TEST_MAPPING
new file mode 100644
index 000000000000..bbc2458f5d8b
--- /dev/null
+++ b/core/tests/coretests/src/android/content/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksCoreTests",
+ "options": [
+ {
+ "include-filter": "android.content.ContentCaptureOptionsTest"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/core/tests/coretests/src/android/view/contentcapture/TEST_MAPPING b/core/tests/coretests/src/android/view/contentcapture/TEST_MAPPING
new file mode 100644
index 000000000000..f8beac2814db
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentcapture/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksCoreTests",
+ "options": [
+ {
+ "include-filter": "android.view.contentcapture"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/core/tests/coretests/src/android/view/contentprotection/TEST_MAPPING b/core/tests/coretests/src/android/view/contentprotection/TEST_MAPPING
new file mode 100644
index 000000000000..3cd4e17d820b
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentprotection/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksCoreTests",
+ "options": [
+ {
+ "include-filter": "android.view.contentprotection"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index a044602f7ec0..b05507e7e128 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -257,6 +257,7 @@ applications that come with the platform
<permission name="android.permission.CLEAR_APP_CACHE"/>
<permission name="android.permission.ACCESS_INSTANT_APPS" />
<permission name="android.permission.CONNECTIVITY_INTERNAL"/>
+ <permission name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
<permission name="android.permission.DELETE_CACHE_FILES"/>
<permission name="android.permission.DELETE_PACKAGES"/>
<permission name="android.permission.DUMP"/>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index d94e8e426c4b..4d73c20fe39f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -17,7 +17,9 @@
package androidx.window.extensions.embedding;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -340,6 +342,20 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.deleteTaskFragment(fragmentToken);
}
+ void reorderTaskFragmentToFront(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REORDER_TO_FRONT).build();
+ wct.addTaskFragmentOperation(fragmentToken, operation);
+ }
+
+ void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, boolean isolatedNav) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build();
+ wct.addTaskFragmentOperation(fragmentToken, operation);
+ }
+
void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index a2f75e099465..f95f3ffb4df3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -250,6 +250,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Updates the Split
final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
final WindowContainerTransaction wct = transactionRecord.getTransaction();
+
+ mPresenter.setTaskFragmentIsolatedNavigation(wct,
+ splitPinContainer.getSecondaryContainer().getTaskFragmentToken(),
+ true /* isolatedNav */);
mPresenter.updateSplitContainer(splitPinContainer, wct);
transactionRecord.apply(false /* shouldApplyIndependently */);
updateCallbackIfNecessary();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 4dafbd17f379..5de6acfcc9db 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -17,7 +17,6 @@
package androidx.window.extensions.embedding;
import static android.content.pm.PackageManager.MATCH_ALL;
-import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
import android.app.Activity;
import android.app.ActivityThread;
@@ -40,7 +39,6 @@ import android.view.WindowInsets;
import android.view.WindowMetrics;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentCreationParams;
-import android.window.TaskFragmentOperation;
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
@@ -427,10 +425,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
final SplitPinContainer pinnedContainer =
container.getTaskContainer().getSplitPinContainer();
if (pinnedContainer != null) {
- final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
- OP_TYPE_REORDER_TO_FRONT).build();
- wct.addTaskFragmentOperation(
- pinnedContainer.getSecondaryContainer().getTaskFragmentToken(), operation);
+ reorderTaskFragmentToFront(wct,
+ pinnedContainer.getSecondaryContainer().getTaskFragmentToken());
}
}
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
index 0ca912e20527..d93e9ba32105 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
@@ -25,9 +25,8 @@
<ImageButton
android:id="@+id/caption_handle"
- android:layout_width="176dp"
+ android:layout_width="128dp"
android:layout_height="42dp"
- android:paddingHorizontal="24dp"
android:paddingVertical="19dp"
android:contentDescription="@string/handle_text"
android:src="@drawable/decor_handle_dark"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 39f861de1ba0..5cf9175073c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -50,7 +50,7 @@ class ActivityEmbeddingAnimationAdapter {
final SurfaceControl mLeash;
/** Area in absolute coordinate that the animation surface shouldn't go beyond. */
@NonNull
- private final Rect mWholeAnimationBounds = new Rect();
+ final Rect mWholeAnimationBounds = new Rect();
/**
* Area in absolute coordinate that should represent all the content to show for this window.
* This should be the end bounds for opening window, and start bounds for closing window in case
@@ -229,20 +229,7 @@ class ActivityEmbeddingAnimationAdapter {
mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y);
t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
t.setAlpha(mLeash, mTransformation.getAlpha());
-
- // The following applies an inverse scale to the clip-rect so that it crops "after" the
- // scale instead of before.
- mVecs[1] = mVecs[2] = 0;
- mVecs[0] = mVecs[3] = 1;
- mTransformation.getMatrix().mapVectors(mVecs);
- mVecs[0] = 1.f / mVecs[0];
- mVecs[3] = 1.f / mVecs[3];
- final Rect clipRect = mTransformation.getClipRect();
- mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
- mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
- mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
- mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
- t.setCrop(mLeash, mRect);
+ t.setWindowCrop(mLeash, mWholeAnimationBounds.width(), mWholeAnimationBounds.height());
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 1793a3d0feb4..4640106b5f1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -26,7 +26,6 @@ import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
-import android.view.animation.ClipRectAnimation;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.ScaleAnimation;
@@ -189,14 +188,6 @@ class ActivityEmbeddingAnimationSpec {
startBounds.top - endBounds.top, 0);
endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
endSet.addAnimation(endTranslate);
- // The end leash is resizing, we should update the window crop based on the clip rect.
- final Rect startClip = new Rect(startBounds);
- final Rect endClip = new Rect(endBounds);
- startClip.offsetTo(0, 0);
- endClip.offsetTo(0, 0);
- final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
- clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
- endSet.addAnimation(clipAnim);
endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
parentBounds.height());
endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
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 20c3bd2dccc4..f5c6a03336ed 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
@@ -203,8 +203,10 @@ public abstract class WMShellModule {
Context context,
@ShellMainThread Handler mainHandler,
@ShellMainThread Choreographer mainChoreographer,
+ ShellInit shellInit,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
+ ShellController shellController,
SyncTransactionQueue syncQueue,
Transitions transitions,
Optional<DesktopModeController> desktopModeController,
@@ -214,8 +216,10 @@ public abstract class WMShellModule {
context,
mainHandler,
mainChoreographer,
+ shellInit,
taskOrganizer,
displayController,
+ shellController,
syncQueue,
transitions,
desktopModeController,
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 6d14440c9b18..f8d7b6bc3aad 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
@@ -202,17 +202,18 @@ class DesktopTasksController(
}
/**
- * Moves a single task to freeform and sets the taskBounds to the passed in bounds,
- * startBounds
+ * The first part of the animated move to desktop transition. Applies the changes to move task
+ * to desktop mode and sets the taskBounds to the passed in bounds, startBounds. This is
+ * followed with a call to {@link finishMoveToDesktop} or {@link cancelMoveToDesktop}.
*/
- fun moveToFreeform(
+ fun startMoveToDesktop(
taskInfo: RunningTaskInfo,
startBounds: Rect,
dragToDesktopValueAnimator: MoveToDesktopAnimator
) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: moveToFreeform with bounds taskId=%d",
+ "DesktopTasksController: startMoveToDesktop taskId=%d",
taskInfo.taskId
)
val wct = WindowContainerTransaction()
@@ -221,18 +222,21 @@ class DesktopTasksController(
wct.setBounds(taskInfo.token, startBounds)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- enterDesktopTaskTransitionHandler.startMoveToFreeformAnimation(wct,
- dragToDesktopValueAnimator, mOnAnimationFinishedCallback)
+ enterDesktopTaskTransitionHandler.startMoveToDesktop(wct, dragToDesktopValueAnimator,
+ mOnAnimationFinishedCallback)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
}
- /** Brings apps to front and sets freeform task bounds */
- private fun moveToDesktopWithAnimation(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
+ /**
+ * The second part of the animated move to desktop transition, called after
+ * {@link startMoveToDesktop}. Brings apps to front and sets freeform task bounds.
+ */
+ private fun finalizeMoveToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: moveToDesktop with animation taskId=%d",
+ "DesktopTasksController: finalizeMoveToDesktop taskId=%d",
taskInfo.taskId
)
val wct = WindowContainerTransaction()
@@ -241,8 +245,8 @@ class DesktopTasksController(
wct.setBounds(taskInfo.token, freeformBounds)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- enterDesktopTaskTransitionHandler.startTransition(
- Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct, mOnAnimationFinishedCallback)
+ enterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct,
+ mOnAnimationFinishedCallback)
} else {
shellTaskOrganizer.applyTransaction(wct)
releaseVisualIndicator()
@@ -272,13 +276,14 @@ class DesktopTasksController(
}
/**
- * Move a task to fullscreen after being dragged from fullscreen and released back into
- * status bar area
+ * The second part of the animated move to desktop transition, called after
+ * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen
+ * and released back into status bar area.
*/
- fun cancelMoveToFreeform(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) {
+ fun cancelMoveToDesktop(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: cancelMoveToFreeform taskId=%d",
+ "DesktopTasksController: cancelMoveToDesktop taskId=%d",
task.taskId
)
val wct = WindowContainerTransaction()
@@ -784,7 +789,7 @@ class DesktopTasksController(
taskInfo: RunningTaskInfo,
freeformBounds: Rect
) {
- moveToDesktopWithAnimation(taskInfo, freeformBounds)
+ finalizeMoveToDesktop(taskInfo, freeformBounds)
}
private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 650cac5cb999..22929c763f27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -79,7 +79,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
* @param wct WindowContainerTransaction for transition
* @param onAnimationEndCallback to be called after animation
*/
- public void startTransition(@WindowManager.TransitionType int type,
+ private void startTransition(@WindowManager.TransitionType int type,
@NonNull WindowContainerTransaction wct,
Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
mOnAnimationFinishedCallback = onAnimationEndCallback;
@@ -88,17 +88,29 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
}
/**
- * Starts Transition of type TRANSIT_ENTER_FREEFORM
+ * Starts Transition of type TRANSIT_START_DRAG_TO_DESKTOP_MODE
* @param wct WindowContainerTransaction for transition
* @param moveToDesktopAnimator Animator that shrinks and positions task during two part move
* to desktop animation
* @param onAnimationEndCallback to be called after animation
*/
- public void startMoveToFreeformAnimation(@NonNull WindowContainerTransaction wct,
+ public void startMoveToDesktop(@NonNull WindowContainerTransaction wct,
@NonNull MoveToDesktopAnimator moveToDesktopAnimator,
Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
mMoveToDesktopAnimator = moveToDesktopAnimator;
- startTransition(Transitions.TRANSIT_ENTER_FREEFORM, wct, onAnimationEndCallback);
+ startTransition(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE, wct,
+ onAnimationEndCallback);
+ }
+
+ /**
+ * Starts Transition of type TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
+ * @param wct WindowContainerTransaction for transition
+ * @param onAnimationEndCallback to be called after animation
+ */
+ public void finalizeMoveToDesktop(@NonNull WindowContainerTransaction wct,
+ Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
+ startTransition(Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE, wct,
+ onAnimationEndCallback);
}
/**
@@ -112,7 +124,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
MoveToDesktopAnimator moveToDesktopAnimator,
Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
mMoveToDesktopAnimator = moveToDesktopAnimator;
- startTransition(Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE, wct,
+ startTransition(Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE, wct,
onAnimationEndCallback);
}
@@ -155,7 +167,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
}
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (type == Transitions.TRANSIT_ENTER_FREEFORM
+ if (type == Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
// Transitioning to freeform but keeping fullscreen bounds, so the crop is set
// to null and we don't require an animation
@@ -182,7 +194,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
}
Rect endBounds = change.getEndAbsBounds();
- if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE
+ if (type == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
&& !endBounds.isEmpty()) {
// This Transition animates a task to freeform bounds after being dragged into freeform
@@ -234,7 +246,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
return true;
}
- if (type == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
+ if (type == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
// This Transition animates a task to fullscreen after being dragged from the status
// bar and then released back into the status bar area
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 65727b6145e4..51e7be0f8a24 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -939,6 +939,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb
* Sets both shelf visibility and its height.
*/
private void setShelfHeight(boolean visible, int height) {
+ if (mEnablePipKeepClearAlgorithm) {
+ // turn this into Launcher keep clear area registration instead
+ setLauncherKeepClearAreaHeight(visible, height);
+ return;
+ }
if (!mIsKeyguardShowingOrAnimating) {
setShelfHeightLocked(visible, height);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index af8ef174b168..7699b4bfd13a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -737,12 +737,23 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
Intent fillInIntent2 = null;
final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2);
+ final ActivityOptions activityOptions1 = options1 != null
+ ? ActivityOptions.fromBundle(options1) : ActivityOptions.makeBasic();
+ final ActivityOptions activityOptions2 = options2 != null
+ ? ActivityOptions.fromBundle(options2) : ActivityOptions.makeBasic();
if (samePackage(packageName1, packageName2, userId1, userId2)) {
if (supportMultiInstancesSplit(packageName1)) {
fillInIntent1 = new Intent();
fillInIntent1.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
fillInIntent2 = new Intent();
fillInIntent2.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+ if (shortcutInfo1 != null) {
+ activityOptions1.setApplyMultipleTaskFlagForShortcut(true);
+ }
+ if (shortcutInfo2 != null) {
+ activityOptions2.setApplyMultipleTaskFlagForShortcut(true);
+ }
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
} else {
pendingIntent2 = null;
@@ -754,9 +765,10 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
Toast.LENGTH_SHORT).show();
}
}
- mStageCoordinator.startIntents(pendingIntent1, fillInIntent1, shortcutInfo1, options1,
- pendingIntent2, fillInIntent2, shortcutInfo2, options2, splitPosition, splitRatio,
- remoteTransition, instanceId);
+ mStageCoordinator.startIntents(pendingIntent1, fillInIntent1, shortcutInfo1,
+ activityOptions1.toBundle(), pendingIntent2, fillInIntent2, shortcutInfo2,
+ activityOptions2.toBundle(), splitPosition, splitRatio, remoteTransition,
+ instanceId);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index e52fd00e7df7..dc78c9b139f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -407,7 +407,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
change.getEndAbsBounds().width(), change.getEndAbsBounds().height());
}
// Rotation change of independent non display window container.
- if (change.getParent() == null
+ if (change.getParent() == null && !change.hasFlags(FLAG_IS_DISPLAY)
&& change.getStartRotation() != change.getEndRotation()) {
startRotationAnimation(startTransaction, change, info,
ROTATION_ANIMATION_ROTATE, animations, onAnimFinish);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index a242c72db8b3..c22cc6fbea8f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -186,9 +186,12 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
final RemoteTransition remoteTransition = mRequestedRemotes.get(mergeTarget);
- final IRemoteTransition remote = remoteTransition.getRemoteTransition();
+ if (remoteTransition == null) return;
+
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Merge into remote: %s",
remoteTransition);
+
+ final IRemoteTransition remote = remoteTransition.getRemoteTransition();
if (remote == null) return;
IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 75659960bc32..e45dacf1189d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -149,17 +149,19 @@ public class Transitions implements RemoteCallable<Transitions>,
/** Transition type for maximize to freeform transition. */
public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
- /** Transition type to freeform in desktop mode. */
- public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10;
+ /** Transition type for starting the move to desktop mode. */
+ public static final int TRANSIT_START_DRAG_TO_DESKTOP_MODE =
+ WindowManager.TRANSIT_FIRST_CUSTOM + 10;
- /** Transition type to freeform in desktop mode. */
- public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11;
+ /** Transition type for finalizing the move to desktop mode. */
+ public static final int TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE =
+ WindowManager.TRANSIT_FIRST_CUSTOM + 11;
/** Transition type to fullscreen from desktop mode. */
public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12;
/** Transition type to animate back to fullscreen when drag to freeform is cancelled. */
- public static final int TRANSIT_CANCEL_ENTERING_DESKTOP_MODE =
+ public static final int TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE =
WindowManager.TRANSIT_FIRST_CUSTOM + 13;
/** Transition type to animate the toggle resize between the max and default desktop sizes. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1a18fc2d7546..2d7e6a602f2f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -72,6 +72,9 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.KeyguardChangeListener;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.TaskCornersListener;
@@ -89,6 +92,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory;
private final ActivityTaskManager mActivityTaskManager;
private final ShellTaskOrganizer mTaskOrganizer;
+ private final ShellController mShellController;
private final Context mContext;
private final Handler mMainHandler;
private final Choreographer mMainChoreographer;
@@ -114,30 +118,37 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private MoveToDesktopAnimator mMoveToDesktopAnimator;
private final Rect mDragToDesktopAnimationStartBounds = new Rect();
+ private final DesktopModeKeyguardChangeListener mDesktopModeKeyguardChangeListener;
public DesktopModeWindowDecorViewModel(
Context context,
Handler mainHandler,
Choreographer mainChoreographer,
+ ShellInit shellInit,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
+ ShellController shellController,
SyncTransactionQueue syncQueue,
Transitions transitions,
Optional<DesktopModeController> desktopModeController,
- Optional<DesktopTasksController> desktopTasksController) {
+ Optional<DesktopTasksController> desktopTasksController
+ ) {
this(
context,
mainHandler,
mainChoreographer,
+ shellInit,
taskOrganizer,
displayController,
+ shellController,
syncQueue,
transitions,
desktopModeController,
desktopTasksController,
new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory(),
- SurfaceControl.Transaction::new);
+ SurfaceControl.Transaction::new,
+ new DesktopModeKeyguardChangeListener());
}
@VisibleForTesting
@@ -145,20 +156,24 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
Context context,
Handler mainHandler,
Choreographer mainChoreographer,
+ ShellInit shellInit,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
+ ShellController shellController,
SyncTransactionQueue syncQueue,
Transitions transitions,
Optional<DesktopModeController> desktopModeController,
Optional<DesktopTasksController> desktopTasksController,
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory,
- Supplier<SurfaceControl.Transaction> transactionFactory) {
+ Supplier<SurfaceControl.Transaction> transactionFactory,
+ DesktopModeKeyguardChangeListener desktopModeKeyguardChangeListener) {
mContext = context;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
+ mShellController = shellController;
mDisplayController = displayController;
mSyncQueue = syncQueue;
mTransitions = transitions;
@@ -168,6 +183,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
mInputMonitorFactory = inputMonitorFactory;
mTransactionFactory = transactionFactory;
+ mDesktopModeKeyguardChangeListener = desktopModeKeyguardChangeListener;
+
+ shellInit.addInitCallback(this::onInit, this);
+ }
+
+ private void onInit() {
+ mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener);
}
@Override
@@ -197,8 +219,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change) {
if (change.getMode() == WindowManager.TRANSIT_CHANGE
- && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE
- || info.getType() == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
+ && (info.getType() == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
+ || info.getType() == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
|| info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE
|| info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE)) {
mWindowDecorByTaskId.get(change.getTaskInfo().taskId)
@@ -616,7 +638,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
} else if (mMoveToDesktopAnimator != null) {
relevantDecor.incrementRelayoutBlock();
mDesktopTasksController.ifPresent(
- c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo,
+ c -> c.cancelMoveToDesktop(relevantDecor.mTaskInfo,
mMoveToDesktopAnimator));
mMoveToDesktopAnimator = null;
return;
@@ -643,7 +665,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo,
relevantDecor.mTaskSurface);
mDesktopTasksController.ifPresent(
- c -> c.moveToFreeform(relevantDecor.mTaskInfo,
+ c -> c.startMoveToDesktop(relevantDecor.mTaskInfo,
mDragToDesktopAnimationStartBounds,
mMoveToDesktopAnimator));
mMoveToDesktopAnimator.startAnimation();
@@ -796,6 +818,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) {
return false;
}
+ if (mDesktopModeKeyguardChangeListener.isKeyguardVisibleAndOccluded()
+ && taskInfo.isFocused) {
+ return false;
+ }
return DesktopModeStatus.isProto2Enabled()
&& taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
&& taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
@@ -884,6 +910,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDesktopTasksController.ifPresent(d -> d.removeCornersForTask(taskId));
}
}
+
+ static class DesktopModeKeyguardChangeListener implements KeyguardChangeListener {
+ private boolean mIsKeyguardVisible;
+ private boolean mIsKeyguardOccluded;
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean visible, boolean occluded,
+ boolean animatingDismiss) {
+ mIsKeyguardVisible = visible;
+ mIsKeyguardOccluded = occluded;
+ }
+
+ public boolean isKeyguardVisibleAndOccluded() {
+ return mIsKeyguardVisible && mIsKeyguardOccluded;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
index fd56a6e49d3e..8a3c2c975faa 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
@@ -42,7 +42,7 @@ import com.android.server.wm.flicker.testapp.ActivityOptions
import com.android.server.wm.flicker.testapp.ActivityOptions.SplitScreen.Primary
import org.junit.Assert.assertNotNull
-internal object SplitScreenUtils {
+object SplitScreenUtils {
private const val TIMEOUT_MS = 3_000L
private const val DRAG_DURATION_MS = 1_000L
private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 0f9579d58929..69c8ecd5644d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.flicker.appcompat
import android.content.Context
-import android.system.helpers.CommandsHelper
import android.tools.common.traces.component.ComponentNameMatcher
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.FlickerTestData
@@ -29,15 +28,18 @@ import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
import com.android.wm.shell.flicker.appWindowKeepVisible
import com.android.wm.shell.flicker.layerKeepVisible
-import org.junit.After
+
import org.junit.Assume
import org.junit.Before
+import org.junit.Rule
abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
protected val context: Context = instrumentation.context
protected val letterboxApp = LetterboxAppHelper(instrumentation)
- lateinit var cmdHelper: CommandsHelper
- private lateinit var letterboxStyle: HashMap<String, String>
+
+ @JvmField
+ @Rule
+ val letterboxRule: LetterboxRule = LetterboxRule()
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
@@ -52,50 +54,7 @@ abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
@Before
fun before() {
- cmdHelper = CommandsHelper.getInstance(instrumentation)
- Assume.assumeTrue(tapl.isTablet && isIgnoreOrientationRequest())
- letterboxStyle = mapLetterboxStyle()
- resetLetterboxStyle()
- setLetterboxEducationEnabled(false)
- }
-
- @After
- fun after() {
- resetLetterboxStyle()
- }
-
- private fun mapLetterboxStyle(): HashMap<String, String> {
- val res = cmdHelper.executeShellCommand("wm get-letterbox-style")
- val lines = res.lines()
- val map = HashMap<String, String>()
- for (line in lines) {
- val keyValuePair = line.split(":")
- if (keyValuePair.size == 2) {
- val key = keyValuePair[0].trim()
- map[key] = keyValuePair[1].trim()
- }
- }
- return map
- }
-
- private fun getLetterboxStyle(): HashMap<String, String> {
- if (!::letterboxStyle.isInitialized) {
- letterboxStyle = mapLetterboxStyle()
- }
- return letterboxStyle
- }
-
- private fun resetLetterboxStyle() {
- cmdHelper.executeShellCommand("wm reset-letterbox-style")
- }
-
- private fun setLetterboxEducationEnabled(enabled: Boolean) {
- cmdHelper.executeShellCommand("wm set-letterbox-style --isEducationEnabled $enabled")
- }
-
- private fun isIgnoreOrientationRequest(): Boolean {
- val res = cmdHelper.executeShellCommand("wm get-ignore-orientation-request")
- return res != null && res.contains("true")
+ Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest)
}
fun FlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation)
@@ -115,7 +74,7 @@ abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
/** Only run on tests with config_letterboxActivityCornersRadius != 0 in devices */
private fun assumeLetterboxRoundedCornersEnabled() {
- Assume.assumeTrue(getLetterboxStyle().getValue("Corner radius") != "0")
+ Assume.assumeTrue(letterboxRule.hasCornerRadius)
}
fun assertLetterboxAppVisibleAtStartAndEnd() {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
new file mode 100644
index 000000000000..5a1136f97c6f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.appcompat
+
+import android.app.Instrumentation
+import android.system.helpers.CommandsHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * JUnit Rule to handle letterboxStyles and states
+ */
+class LetterboxRule(
+ private val withLetterboxEducationEnabled: Boolean = false,
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+ private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
+) : TestRule {
+
+ private val execAdb: (String) -> String = {cmd -> cmdHelper.executeShellCommand(cmd)}
+ private lateinit var _letterboxStyle: MutableMap<String, String>
+
+ val letterboxStyle: Map<String, String>
+ get() {
+ if (!::_letterboxStyle.isInitialized) {
+ _letterboxStyle = mapLetterboxStyle()
+ }
+ return _letterboxStyle
+ }
+
+ val cornerRadius: Int?
+ get() = asInt(letterboxStyle["Corner radius"])
+
+ val hasCornerRadius: Boolean
+ get() {
+ val radius = cornerRadius
+ return radius != null && radius > 0
+ }
+
+ val isIgnoreOrientationRequest: Boolean
+ get() = execAdb("wm get-ignore-orientation-request")?.contains("true") ?: false
+
+ override fun apply(base: Statement?, description: Description?): Statement {
+ resetLetterboxStyle()
+ _letterboxStyle = mapLetterboxStyle()
+ val isLetterboxEducationEnabled = _letterboxStyle.getValue("Is education enabled")
+ var hasLetterboxEducationStateChanged = false
+ if ("$withLetterboxEducationEnabled" != isLetterboxEducationEnabled) {
+ hasLetterboxEducationStateChanged = true
+ execAdb("wm set-letterbox-style --isEducationEnabled " +
+ withLetterboxEducationEnabled)
+ }
+ return try {
+ object : Statement() {
+ @Throws(Throwable::class)
+ override fun evaluate() {
+ base!!.evaluate()
+ }
+ }
+ } finally {
+ if (hasLetterboxEducationStateChanged) {
+ execAdb("wm set-letterbox-style --isEducationEnabled " +
+ isLetterboxEducationEnabled
+ )
+ }
+ resetLetterboxStyle()
+ }
+ }
+
+ private fun mapLetterboxStyle(): HashMap<String, String> {
+ val res = execAdb("wm get-letterbox-style")
+ val lines = res.lines()
+ val map = HashMap<String, String>()
+ for (line in lines) {
+ val keyValuePair = line.split(":")
+ if (keyValuePair.size == 2) {
+ val key = keyValuePair[0].trim()
+ map[key] = keyValuePair[1].trim()
+ }
+ }
+ return map
+ }
+
+ private fun resetLetterboxStyle() {
+ execAdb("wm reset-letterbox-style")
+ }
+
+ private fun asInt(str: String?): Int? = try {
+ str?.toInt()
+ } catch (e: NumberFormatException) {
+ null
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
index a7bd2584ba23..67d5718e6c1f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
@@ -31,7 +31,7 @@ import org.junit.runners.Parameterized
/**
* Test launching app in size compat mode.
*
- * To run this test: `atest WMShellFlickerTests:OpenAppInSizeCompatModeTest`
+ * To run this test: `atest WMShellFlickerTestsOther:OpenAppInSizeCompatModeTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
new file mode 100644
index 000000000000..e6ca261a317f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.appcompat
+
+import android.platform.test.annotations.Postsubmit
+import android.tools.common.Rotation
+import android.tools.common.flicker.assertions.FlickerTest
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching app in size compat mode.
+ *
+ * To run this test: `atest WMShellFlickerTestsOther:OpenTransparentActivityTest`
+ *
+ * Actions:
+ * ```
+ * Launch a letteboxed app and then a transparent activity from it. We test the bounds
+ * are the same.
+ * ```
+ *
+ * Notes:
+ * ```
+ * Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited [BaseTest]
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+class OpenTransparentActivityTest(flicker: LegacyFlickerTest) : TransparentBaseAppCompat(flicker) {
+
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ letterboxTranslucentLauncherApp.launchViaIntent(wmHelper)
+ }
+ transitions {
+ waitAndGetLaunchTransparent()?.click() ?: error("Launch Transparent not found")
+ }
+ teardown {
+ letterboxTranslucentApp.exit(wmHelper)
+ letterboxTranslucentLauncherApp.exit(wmHelper)
+ }
+ }
+
+ /**
+ * Checks the transparent activity is launched on top of the opaque one
+ */
+ @Postsubmit
+ @Test
+ fun translucentActivityIsLaunchedOnTopOfOpaqueActivity() {
+ flicker.assertWm {
+ this.isAppWindowOnTop(letterboxTranslucentLauncherApp)
+ .then()
+ .isAppWindowOnTop(letterboxTranslucentApp)
+ }
+ }
+
+ /**
+ * Checks that the activity is letterboxed
+ */
+ @Postsubmit
+ @Test
+ fun translucentActivityIsLetterboxed() {
+ flicker.assertLayers { isVisible(ComponentNameMatcher.LETTERBOX) }
+ }
+
+ /**
+ * Checks that the translucent activity inherits bounds from the opaque one.
+ */
+ @Postsubmit
+ @Test
+ fun translucentActivityInheritsBoundsFromOpaqueActivity() {
+ flicker.assertLayersEnd {
+ this.visibleRegion(letterboxTranslucentApp)
+ .coversExactly(visibleRegion(letterboxTranslucentLauncherApp).region)
+ }
+ }
+
+ /**
+ * Checks that the translucent activity has rounded corners
+ */
+ @Postsubmit
+ @Test
+ fun translucentActivityHasRoundedCorners() {
+ flicker.assertLayersEnd {
+ this.hasRoundedCorners(letterboxTranslucentApp)
+ }
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [FlickerTestFactory.rotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams(): Collection<FlickerTest> {
+ return LegacyFlickerTestFactory
+ .nonRotationTests(supportedRotations = listOf(Rotation.ROTATION_90))
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
index e875aae431a1..68fa8d2fc2e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
@@ -32,7 +32,9 @@ import org.junit.runners.Parameterized
/**
* Test launching a fixed portrait letterboxed app in landscape and repositioning to the right.
*
- * To run this test: `atest WMShellFlickerTests:RepositionFixedPortraitAppTest` Actions:
+ * To run this test: `atest WMShellFlickerTestsOther:RepositionFixedPortraitAppTest`
+ *
+ * Actions:
*
* ```
* Launch a fixed portrait app in landscape to letterbox app
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
index a18a144b4bf1..fcb6931af9a2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt
@@ -31,7 +31,7 @@ import org.junit.runners.Parameterized
/**
* Test restarting app in size compat mode.
*
- * To run this test: `atest WMShellFlickerTests:RestartAppInSizeCompatModeTest`
+ * To run this test: `atest WMShellFlickerTestsOther:RestartAppInSizeCompatModeTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
new file mode 100644
index 000000000000..ea0392cee95a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.appcompat
+
+import android.content.Context
+import android.tools.device.flicker.legacy.FlickerTestData
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.helpers.FIND_TIMEOUT
+import android.tools.device.traces.parsers.toFlickerComponent
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiObject2
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.helpers.LetterboxAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.wm.shell.flicker.BaseTest
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+
+abstract class TransparentBaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
+ protected val context: Context = instrumentation.context
+ protected val letterboxTranslucentLauncherApp = LetterboxAppHelper(
+ instrumentation,
+ launcherName = ActivityOptions.LaunchTransparentActivity.LABEL,
+ component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent()
+ )
+ protected val letterboxTranslucentApp = LetterboxAppHelper(
+ instrumentation,
+ launcherName = ActivityOptions.TransparentActivity.LABEL,
+ component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent()
+ )
+
+ @JvmField
+ @Rule
+ val letterboxRule: LetterboxRule = LetterboxRule()
+
+ @Before
+ fun before() {
+ Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest)
+ }
+
+ protected fun FlickerTestData.waitAndGetLaunchTransparent(): UiObject2? =
+ device.wait(
+ Until.findObject(By.text("Launch Transparent")),
+ FIND_TIMEOUT
+ )
+
+ protected fun FlickerTestData.goBack() = device.pressBack()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
index c6642f3472f0..772d97d8eb32 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java
@@ -99,16 +99,17 @@ public class EnterDesktopTaskTransitionHandlerTest {
final int taskId = 1;
WindowContainerTransaction wct = new WindowContainerTransaction();
doReturn(mToken).when(mTransitions)
- .startTransition(Transitions.TRANSIT_ENTER_FREEFORM, wct,
+ .startTransition(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE, wct,
mEnterDesktopTaskTransitionHandler);
doReturn(taskId).when(mMoveToDesktopAnimator).getTaskId();
- mEnterDesktopTaskTransitionHandler.startMoveToFreeformAnimation(wct,
+ mEnterDesktopTaskTransitionHandler.startMoveToDesktop(wct,
mMoveToDesktopAnimator, null);
TransitionInfo.Change change =
createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
- TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change);
+ TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE,
+ change);
assertTrue(mEnterDesktopTaskTransitionHandler
@@ -120,17 +121,18 @@ public class EnterDesktopTaskTransitionHandlerTest {
@Test
public void testTransitEnterDesktopModeAnimation() throws Throwable {
- final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE;
+ final int transitionType = Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE;
final int taskId = 1;
WindowContainerTransaction wct = new WindowContainerTransaction();
doReturn(mToken).when(mTransitions)
.startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler);
- mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct, null);
+ mEnterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct, null);
TransitionInfo.Change change =
createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM);
change.setEndAbsBounds(new Rect(0, 0, 1, 1));
- TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change);
+ TransitionInfo info = createTransitionInfo(
+ Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE, change);
runOnUiThread(() -> {
try {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
index adc2a6fbff23..596d6dd3a3d2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
@@ -18,12 +18,15 @@ package com.android.wm.shell.windowdecor;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -53,6 +56,8 @@ import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopModeController;
import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -91,6 +96,10 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
@Mock private Supplier<SurfaceControl.Transaction> mTransactionFactory;
@Mock private SurfaceControl.Transaction mTransaction;
@Mock private Display mDisplay;
+ @Mock private ShellController mShellController;
+ @Mock private ShellInit mShellInit;
+ @Mock private DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
+ mDesktopModeKeyguardChangeListener;
private final List<InputManager> mMockInputManagers = new ArrayList<>();
private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel;
@@ -104,15 +113,18 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
mContext,
mMainHandler,
mMainChoreographer,
+ mShellInit,
mTaskOrganizer,
mDisplayController,
+ mShellController,
mSyncQueue,
mTransitions,
Optional.of(mDesktopModeController),
Optional.of(mDesktopTasksController),
mDesktopModeWindowDecorFactory,
mMockInputMonitorFactory,
- mTransactionFactory
+ mTransactionFactory,
+ mDesktopModeKeyguardChangeListener
);
doReturn(mDesktopModeWindowDecoration)
@@ -121,6 +133,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
doReturn(mTransaction).when(mTransactionFactory).get();
doReturn(mDisplayLayout).when(mDisplayController).getDisplayLayout(anyInt());
doReturn(STABLE_INSETS).when(mDisplayLayout).stableInsets();
+ doNothing().when(mShellController).addKeyguardChangeListener(any());
when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor);
// InputChannel cannot be mocked because it passes to InputEventReceiver.
@@ -255,6 +268,32 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase {
verify(mInputMonitor, times(1)).dispose();
}
+ @Test
+ public void testCaptionIsNotCreatedWhenKeyguardIsVisible() throws Exception {
+ doReturn(true).when(
+ mDesktopModeKeyguardChangeListener).isKeyguardVisibleAndOccluded();
+
+ final int taskId = 1;
+ final ActivityManager.RunningTaskInfo taskInfo =
+ createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN);
+ taskInfo.isFocused = true;
+ SurfaceControl surfaceControl = mock(SurfaceControl.class);
+ runOnMainThread(() -> {
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+ mDesktopModeWindowDecorViewModel.onTaskOpening(
+ taskInfo, surfaceControl, startT, finishT);
+
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED);
+ mDesktopModeWindowDecorViewModel.onTaskChanging(
+ taskInfo, surfaceControl, startT, finishT);
+ });
+ verify(mDesktopModeWindowDecorFactory, never())
+ .create(any(), any(), any(), any(), any(), any(), any(), any());
+ }
+
private void runOnMainThread(Runnable r) throws Exception {
final Handler mainHandler = new Handler(Looper.getMainLooper());
final CountDownLatch latch = new CountDownLatch(1);
diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index f71e7289bd37..b87002371775 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -88,6 +88,9 @@ void ShaderCache::initShaderDiskCache(const void* identity, ssize_t size) {
mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, maxTotalSize, mFilename));
validateCache(identity, size);
mInitialized = true;
+ if (identity != nullptr && size > 0 && mIDHash.size()) {
+ set(&sIDKey, sizeof(sIDKey), mIDHash.data(), mIDHash.size());
+ }
}
}
@@ -96,11 +99,6 @@ void ShaderCache::setFilename(const char* filename) {
mFilename = filename;
}
-BlobCache* ShaderCache::getBlobCacheLocked() {
- LOG_ALWAYS_FATAL_IF(!mInitialized, "ShaderCache has not been initialized");
- return mBlobCache.get();
-}
-
sk_sp<SkData> ShaderCache::load(const SkData& key) {
ATRACE_NAME("ShaderCache::load");
size_t keySize = key.size();
@@ -115,8 +113,7 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) {
if (!valueBuffer) {
return nullptr;
}
- BlobCache* bc = getBlobCacheLocked();
- size_t valueSize = bc->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
+ size_t valueSize = mBlobCache->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
int maxTries = 3;
while (valueSize > mObservedBlobValueSize && maxTries > 0) {
mObservedBlobValueSize = std::min(valueSize, maxValueSize);
@@ -126,7 +123,7 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) {
return nullptr;
}
valueBuffer = newValueBuffer;
- valueSize = bc->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
+ valueSize = mBlobCache->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize);
maxTries--;
}
if (!valueSize) {
@@ -143,16 +140,17 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) {
return SkData::MakeFromMalloc(valueBuffer, valueSize);
}
-namespace {
-// Helper for BlobCache::set to trace the result.
-void set(BlobCache* cache, const void* key, size_t keySize, const void* value, size_t valueSize) {
- switch (cache->set(key, keySize, value, valueSize)) {
+void ShaderCache::set(const void* key, size_t keySize, const void* value, size_t valueSize) {
+ switch (mBlobCache->set(key, keySize, value, valueSize)) {
case BlobCache::InsertResult::kInserted:
// This is what we expect/hope. It means the cache is large enough.
return;
case BlobCache::InsertResult::kDidClean: {
ATRACE_FORMAT("ShaderCache: evicted an entry to fit {key: %lu value %lu}!", keySize,
valueSize);
+ if (mIDHash.size()) {
+ set(&sIDKey, sizeof(sIDKey), mIDHash.data(), mIDHash.size());
+ }
return;
}
case BlobCache::InsertResult::kNotEnoughSpace: {
@@ -172,15 +170,10 @@ void set(BlobCache* cache, const void* key, size_t keySize, const void* value, s
}
}
}
-} // namespace
void ShaderCache::saveToDiskLocked() {
ATRACE_NAME("ShaderCache::saveToDiskLocked");
if (mInitialized && mBlobCache) {
- if (mIDHash.size()) {
- auto key = sIDKey;
- set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size());
- }
// The most straightforward way to make ownership shared
mMutex.unlock();
mMutex.lock_shared();
@@ -209,11 +202,10 @@ void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /
const void* value = data.data();
- BlobCache* bc = getBlobCacheLocked();
if (mInStoreVkPipelineInProgress) {
if (mOldPipelineCacheSize == -1) {
// Record the initial pipeline cache size stored in the file.
- mOldPipelineCacheSize = bc->get(key.data(), keySize, nullptr, 0);
+ mOldPipelineCacheSize = mBlobCache->get(key.data(), keySize, nullptr, 0);
}
if (mNewPipelineCacheSize != -1 && mNewPipelineCacheSize == valueSize) {
// There has not been change in pipeline cache size. Stop trying to save.
@@ -228,7 +220,7 @@ void ShaderCache::store(const SkData& key, const SkData& data, const SkString& /
mNewPipelineCacheSize = -1;
mTryToStorePipelineCache = true;
}
- set(bc, key.data(), keySize, value, valueSize);
+ set(key.data(), keySize, value, valueSize);
if (!mSavePending && mDeferredSaveDelayMs > 0) {
mSavePending = true;
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index 2f91c778b8a0..74955503dbb1 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -96,20 +96,18 @@ private:
void operator=(const ShaderCache&) = delete;
/**
- * "getBlobCacheLocked" returns the BlobCache object being used to store the
- * key/value blob pairs. If the BlobCache object has not yet been created,
- * this will do so, loading the serialized cache contents from disk if
- * possible.
- */
- BlobCache* getBlobCacheLocked() REQUIRES(mMutex);
-
- /**
* "validateCache" updates the cache to match the given identity. If the
* cache currently has the wrong identity, all entries in the cache are cleared.
*/
bool validateCache(const void* identity, ssize_t size) REQUIRES(mMutex);
/**
+ * Helper for BlobCache::set to trace the result and ensure the identity hash
+ * does not get evicted.
+ */
+ void set(const void* key, size_t keySize, const void* value, size_t valueSize) REQUIRES(mMutex);
+
+ /**
* "saveToDiskLocked" attempts to save the current contents of the cache to
* disk. If the identity hash exists, we will insert the identity hash into
* the cache for next validation.
@@ -127,11 +125,9 @@ private:
bool mInitialized GUARDED_BY(mMutex) = false;
/**
- * "mBlobCache" is the cache in which the key/value blob pairs are stored. It
- * is initially NULL, and will be initialized by getBlobCacheLocked the
- * first time it's needed.
- * The blob cache contains the Android build number. We treat version mismatches as an empty
- * cache (logic implemented in BlobCache::unflatten).
+ * "mBlobCache" is the cache in which the key/value blob pairs are stored.
+ * The blob cache contains the Android build number. We treat version mismatches
+ * as an empty cache (logic implemented in BlobCache::unflatten).
*/
std::unique_ptr<FileBlobCache> mBlobCache GUARDED_BY(mMutex);
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 31a92ac5ab23..46698a6fdcc0 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -40,7 +40,7 @@ namespace android {
namespace uirenderer {
namespace renderthread {
-static std::array<std::string_view, 19> sEnableExtensions{
+static std::array<std::string_view, 20> sEnableExtensions{
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -60,6 +60,7 @@ static std::array<std::string_view, 19> sEnableExtensions{
VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME,
VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
+ VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
};
static bool shouldEnableExtension(const std::string_view& extension) {
diff --git a/media/java/android/media/audiofx/Visualizer.java b/media/java/android/media/audiofx/Visualizer.java
index f3dfeb665cc7..2795cfe4ba61 100644
--- a/media/java/android/media/audiofx/Visualizer.java
+++ b/media/java/android/media/audiofx/Visualizer.java
@@ -455,11 +455,13 @@ public class Visualizer {
* a number of consecutive 8-bit (unsigned) mono PCM samples equal to the capture size returned
* by {@link #getCaptureSize()}.
* <p>This method must be called when the Visualizer is enabled.
- * @param waveform array of bytes where the waveform should be returned
+ * @param waveform array of bytes where the waveform should be returned, array length must be
+ * at least equals to the capture size returned by {@link #getCaptureSize()}.
* @return {@link #SUCCESS} in case of success,
* {@link #ERROR_NO_MEMORY}, {@link #ERROR_INVALID_OPERATION} or {@link #ERROR_DEAD_OBJECT}
* in case of failure.
* @throws IllegalStateException
+ * @throws IllegalArgumentException
*/
public int getWaveForm(byte[] waveform)
throws IllegalStateException {
@@ -467,6 +469,12 @@ public class Visualizer {
if (mState != STATE_ENABLED) {
throw(new IllegalStateException("getWaveForm() called in wrong state: "+mState));
}
+ int captureSize = getCaptureSize();
+ if (captureSize > waveform.length) {
+ throw(new IllegalArgumentException("getWaveForm() called with illegal size: "
+ + waveform.length + " expecting at least "
+ + captureSize + " bytes"));
+ }
return native_getWaveForm(waveform);
}
}
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index dea7f03d369a..5ea98c0c6700 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2615,7 +2615,7 @@ static void android_media_MediaCodec_native_queueLinearBlock(
return;
}
auto cryptoInfo =
- cryptoInfoObj ? NativeCryptoInfo{size} : NativeCryptoInfo{env, cryptoInfoObj};
+ cryptoInfoObj ? NativeCryptoInfo{env, cryptoInfoObj} : NativeCryptoInfo{size};
if (env->ExceptionCheck()) {
// Creation of cryptoInfo failed. Let the exception bubble up.
return;
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
index 9b33704cc8e7..eccf6047b90c 100644
--- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiver.java
@@ -191,6 +191,8 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED)
&& isPendingIntentValid(intent,
SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION)
+ && isPendingIntentValid(intent,
+ SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED)
&& isPendingIntentValid(intent, SlicePurchaseController.EXTRA_INTENT_SUCCESS)
&& isPendingIntentValid(intent,
SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
@@ -276,6 +278,8 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
case SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED: return "request failed";
case SlicePurchaseController.EXTRA_INTENT_NOT_DEFAULT_DATA_SUBSCRIPTION:
return "not default data subscription";
+ case SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED:
+ return "notifications disabled";
case SlicePurchaseController.EXTRA_INTENT_SUCCESS: return "success";
case SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN:
return "notification shown";
@@ -321,26 +325,45 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
}
private void onDisplayPerformanceBoostNotification(@NonNull Context context,
- @NonNull Intent intent, boolean repeat) {
- if (!repeat && !isIntentValid(intent)) {
+ @NonNull Intent intent, boolean localeChanged) {
+ if (!localeChanged && !isIntentValid(intent)) {
sendSlicePurchaseAppResponse(intent,
SlicePurchaseController.EXTRA_INTENT_REQUEST_FAILED);
return;
}
Resources res = getResources(context);
- NotificationChannel channel = new NotificationChannel(
- PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID,
- res.getString(R.string.performance_boost_notification_channel),
- NotificationManager.IMPORTANCE_DEFAULT);
- // CarrierDefaultApp notifications are unblockable by default. Make this channel blockable
- // to allow users to disable notifications posted to this channel without affecting other
- // notifications in this application.
- channel.setBlockable(true);
- context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
+ NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+ NotificationChannel channel = notificationManager.getNotificationChannel(
+ PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID);
+ if (channel == null) {
+ channel = new NotificationChannel(
+ PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID,
+ res.getString(R.string.performance_boost_notification_channel),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ // CarrierDefaultApp notifications are unblockable by default.
+ // Make this channel blockable to allow users to disable notifications posted to this
+ // channel without affecting other notifications in this application.
+ channel.setBlockable(true);
+ context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
+ } else if (localeChanged) {
+ // If the channel already exists but the locale has changed, update the channel name.
+ channel.setName(res.getString(R.string.performance_boost_notification_channel));
+ }
- String carrier = intent.getStringExtra(SlicePurchaseController.EXTRA_CARRIER);
+ boolean channelNotificationsDisabled =
+ channel.getImportance() == NotificationManager.IMPORTANCE_NONE;
+ if (channelNotificationsDisabled || !notificationManager.areNotificationsEnabled()) {
+ // If notifications are disabled for the app or channel, fail the purchase request.
+ logd("Purchase request failed because notifications are disabled for the "
+ + (channelNotificationsDisabled ? "channel." : "application."));
+ sendSlicePurchaseAppResponse(intent,
+ SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED);
+ return;
+ }
+ String carrier = intent.getStringExtra(SlicePurchaseController.EXTRA_CARRIER);
Notification notification =
new Notification.Builder(context, PERFORMANCE_BOOST_NOTIFICATION_CHANNEL_ID)
.setContentTitle(res.getString(
@@ -369,11 +392,12 @@ public class SlicePurchaseBroadcastReceiver extends BroadcastReceiver{
int capability = intent.getIntExtra(SlicePurchaseController.EXTRA_PREMIUM_CAPABILITY,
SlicePurchaseController.PREMIUM_CAPABILITY_INVALID);
- logd((repeat ? "Update" : "Display") + " the performance boost notification for capability "
+ logd((localeChanged ? "Update" : "Display")
+ + " the performance boost notification for capability "
+ TelephonyManager.convertPremiumCapabilityToString(capability));
context.getSystemService(NotificationManager.class).notifyAsUser(
PERFORMANCE_BOOST_NOTIFICATION_TAG, capability, notification, UserHandle.ALL);
- if (!repeat) {
+ if (!localeChanged) {
sIntents.put(capability, intent);
sendSlicePurchaseAppResponse(intent,
SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
index 61847b517c8d..3c8ef6ed0550 100644
--- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/SlicePurchaseBroadcastReceiverTest.java
@@ -32,6 +32,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
@@ -72,6 +73,7 @@ public class SlicePurchaseBroadcastReceiverTest {
@Mock PendingIntent mContentIntent1;
@Mock PendingIntent mContentIntent2;
@Mock PendingIntent mNotificationShownIntent;
+ @Mock PendingIntent mNotificationsDisabledIntent;
@Mock Context mContext;
@Mock Resources mResources;
@Mock Configuration mConfiguration;
@@ -90,6 +92,7 @@ public class SlicePurchaseBroadcastReceiverTest {
doReturn("").when(mResources).getString(anyInt());
doReturn(mNotificationManager).when(mContext)
.getSystemService(eq(NotificationManager.class));
+ doReturn(true).when(mNotificationManager).areNotificationsEnabled();
doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
doReturn(mPackageManager).when(mContext).getPackageManager();
doReturn(mSpiedResources).when(mContext).getResources();
@@ -221,12 +224,10 @@ public class SlicePurchaseBroadcastReceiverTest {
doReturn(true).when(mPendingIntent).isBroadcast();
doReturn(mPendingIntent).when(mIntent).getParcelableExtra(
anyString(), eq(PendingIntent.class));
- doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(mNotificationShownIntent)
- .getCreatorPackage();
- doReturn(true).when(mNotificationShownIntent).isBroadcast();
- doReturn(mNotificationShownIntent).when(mIntent).getParcelableExtra(
- eq(SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN),
- eq(PendingIntent.class));
+ createValidPendingIntent(mNotificationShownIntent,
+ SlicePurchaseController.EXTRA_INTENT_NOTIFICATION_SHOWN);
+ createValidPendingIntent(mNotificationsDisabledIntent,
+ SlicePurchaseController.EXTRA_INTENT_NOTIFICATIONS_DISABLED);
// spy notification intents to prevent PendingIntent issues
doReturn(mContentIntent1).when(mSlicePurchaseBroadcastReceiver).createContentIntent(
@@ -253,6 +254,12 @@ public class SlicePurchaseBroadcastReceiverTest {
mSlicePurchaseBroadcastReceiver.onReceive(mContext, mIntent);
}
+ private void createValidPendingIntent(@NonNull PendingIntent intent, @NonNull String extra) {
+ doReturn(TelephonyManager.PHONE_PROCESS_NAME).when(intent).getCreatorPackage();
+ doReturn(true).when(intent).isBroadcast();
+ doReturn(intent).when(mIntent).getParcelableExtra(eq(extra), eq(PendingIntent.class));
+ }
+
@Test
public void testNotificationCanceled() {
// send ACTION_NOTIFICATION_CANCELED
@@ -335,4 +342,22 @@ public class SlicePurchaseBroadcastReceiverTest {
clearInvocations(mConfiguration);
return captor.getValue();
}
+
+ @Test
+ public void testNotificationsDisabled() throws Exception {
+ doReturn(false).when(mNotificationManager).areNotificationsEnabled();
+
+ displayPerformanceBoostNotification();
+
+ // verify notification was not shown
+ verify(mNotificationManager, never()).notifyAsUser(
+ eq(SlicePurchaseBroadcastReceiver.PERFORMANCE_BOOST_NOTIFICATION_TAG),
+ eq(TelephonyManager.PREMIUM_CAPABILITY_PRIORITIZE_LATENCY),
+ any(),
+ eq(UserHandle.ALL));
+ verify(mNotificationShownIntent, never()).send();
+
+ // verify SlicePurchaseController was notified that notifications are disabled
+ verify(mNotificationsDisabledIntent).send();
+ }
}
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
index e1eb36ac276c..25ac3c9d9074 100644
--- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
+++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java
@@ -419,12 +419,20 @@ public class DynamicSystemInstallationService extends Service
mDynSystem.remove();
}
+ private boolean isDsuSlotLocked() {
+ // Slot names ending with ".lock" are a customized installation.
+ // We expect the client app to provide custom UI to enter/exit DSU mode.
+ // We will ignore the ACTION_REBOOT_TO_NORMAL command and will not show
+ // notifications in this case.
+ return mDynSystem.getActiveDsuSlot().endsWith(".lock");
+ }
+
private void executeRebootToNormalCommand() {
if (!isInDynamicSystem()) {
Log.e(TAG, "It's already running in normal system.");
return;
}
- if (mDynSystem.getActiveDsuSlot().endsWith(".lock")) {
+ if (isDsuSlotLocked()) {
Log.e(TAG, "Ignore the reboot intent for a locked DSU slot");
return;
}
@@ -449,13 +457,13 @@ public class DynamicSystemInstallationService extends Service
private void executeNotifyIfInUseCommand() {
switch (getStatus()) {
case STATUS_IN_USE:
- if (!mHideNotification) {
+ if (!mHideNotification && !isDsuSlotLocked()) {
startForeground(NOTIFICATION_ID,
buildNotification(STATUS_IN_USE, CAUSE_NOT_SPECIFIED));
}
break;
case STATUS_READY:
- if (!mHideNotification) {
+ if (!mHideNotification && !isDsuSlotLocked()) {
startForeground(NOTIFICATION_ID,
buildNotification(STATUS_READY, CAUSE_NOT_SPECIFIED));
}
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
index 00dd8cc88da2..1a938d6ec37e 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java
@@ -179,7 +179,8 @@ public abstract class Tile implements Parcelable {
* Check whether tile has order.
*/
public boolean hasOrder() {
- return mMetaData.containsKey(META_DATA_KEY_ORDER)
+ return mMetaData != null
+ && mMetaData.containsKey(META_DATA_KEY_ORDER)
&& mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer;
}
@@ -204,7 +205,7 @@ public abstract class Tile implements Parcelable {
CharSequence title = null;
ensureMetadataNotStale(context);
final PackageManager packageManager = context.getPackageManager();
- if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
+ if (mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
// If has as uri to provide dynamic title, skip loading here. UI will later load
// at tile binding time.
@@ -284,10 +285,10 @@ public abstract class Tile implements Parcelable {
* Optional key to use for this tile.
*/
public String getKey(Context context) {
+ ensureMetadataNotStale(context);
if (!hasKey()) {
return null;
}
- ensureMetadataNotStale(context);
if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT));
} else {
diff --git a/packages/SettingsLib/res/drawable/ic_hotspot_auto.xml b/packages/SettingsLib/res/drawable/ic_hotspot_auto.xml
new file mode 100644
index 000000000000..ddd526ada46d
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_hotspot_auto.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M240,760L240,800Q240,817 228.5,828.5Q217,840 200,840L160,840Q143,840 131.5,828.5Q120,817 120,800L120,480L204,240Q210,222 225.5,211Q241,200 260,200L700,200Q719,200 734.5,211Q750,222 756,240L840,480L840,800Q840,817 828.5,828.5Q817,840 800,840L760,840Q743,840 731.5,828.5Q720,817 720,800L720,760L240,760ZM232,400L728,400L686,280L274,280L232,400ZM200,480L200,480L200,680L200,680L200,480ZM300,640Q325,640 342.5,622.5Q360,605 360,580Q360,555 342.5,537.5Q325,520 300,520Q275,520 257.5,537.5Q240,555 240,580Q240,605 257.5,622.5Q275,640 300,640ZM660,640Q685,640 702.5,622.5Q720,605 720,580Q720,555 702.5,537.5Q685,520 660,520Q635,520 617.5,537.5Q600,555 600,580Q600,605 617.5,622.5Q635,640 660,640ZM200,680L760,680L760,480L200,480L200,680Z"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_hotspot_laptop.xml b/packages/SettingsLib/res/drawable/ic_hotspot_laptop.xml
new file mode 100644
index 000000000000..5e1b184c2a5a
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_hotspot_laptop.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M0,800L0,720L80,720L80,120L880,120L880,720L960,720L960,800L0,800ZM400,720L560,720L560,680L400,680L400,720ZM160,600L800,600L800,200L160,200L160,600ZM160,600L160,200L160,200L160,600L160,600Z"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_hotspot_phone.xml b/packages/SettingsLib/res/drawable/ic_hotspot_phone.xml
new file mode 100644
index 000000000000..baa793c0e9c1
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_hotspot_phone.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M280,920Q247,920 223.5,896.5Q200,873 200,840L200,120Q200,87 223.5,63.5Q247,40 280,40L680,40Q713,40 736.5,63.5Q760,87 760,120L760,840Q760,873 736.5,896.5Q713,920 680,920L280,920ZM280,800L280,840Q280,840 280,840Q280,840 280,840L680,840Q680,840 680,840Q680,840 680,840L680,800L280,800ZM280,720L680,720L680,240L280,240L280,720ZM280,160L680,160L680,120Q680,120 680,120Q680,120 680,120L280,120Q280,120 280,120Q280,120 280,120L280,160ZM280,160L280,120Q280,120 280,120Q280,120 280,120L280,120Q280,120 280,120Q280,120 280,120L280,160L280,160ZM280,800L280,800L280,840Q280,840 280,840Q280,840 280,840L280,840Q280,840 280,840Q280,840 280,840L280,800Z"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_hotspot_tablet.xml b/packages/SettingsLib/res/drawable/ic_hotspot_tablet.xml
new file mode 100644
index 000000000000..cf67cd9e2b77
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_hotspot_tablet.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M120,800Q87,800 63.5,776.5Q40,753 40,720L40,240Q40,207 63.5,183.5Q87,160 120,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,720Q920,753 896.5,776.5Q873,800 840,800L120,800ZM160,240L120,240Q120,240 120,240Q120,240 120,240L120,720Q120,720 120,720Q120,720 120,720L160,720L160,240ZM240,720L720,720L720,240L240,240L240,720ZM800,240L800,720L840,720Q840,720 840,720Q840,720 840,720L840,240Q840,240 840,240Q840,240 840,240L800,240ZM800,240L840,240Q840,240 840,240Q840,240 840,240L840,240Q840,240 840,240Q840,240 840,240L800,240L800,240ZM160,240L160,240L120,240Q120,240 120,240Q120,240 120,240L120,240Q120,240 120,240Q120,240 120,240L160,240Z"/>
+</vector>
diff --git a/packages/SettingsLib/res/drawable/ic_hotspot_watch.xml b/packages/SettingsLib/res/drawable/ic_hotspot_watch.xml
new file mode 100644
index 000000000000..252a0dbd62b2
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_hotspot_watch.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M360,880L306,698Q258,660 229,603Q200,546 200,480Q200,414 229,357Q258,300 306,262L360,80L600,80L654,262Q702,300 731,357Q760,414 760,480Q760,546 731,603Q702,660 654,698L600,880L360,880ZM480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280Q397,280 338.5,338.5Q280,397 280,480Q280,563 338.5,621.5Q397,680 480,680ZM404,210Q424,205 442.5,202Q461,199 480,199Q499,199 517.5,202Q536,205 556,210L540,160L420,160L404,210ZM420,800L540,800L556,750Q536,755 517.5,757.5Q499,760 480,760Q461,760 442.5,757.5Q424,755 404,750L420,800ZM404,160L420,160L540,160L556,160Q536,160 517.5,160Q499,160 480,160Q461,160 442.5,160Q424,160 404,160ZM420,800L404,800Q424,800 442.5,800Q461,800 480,800Q499,800 517.5,800Q536,800 556,800L540,800L420,800Z"/>
+</vector>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index f522fd13c9f8..2118d2cbf4b3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -356,11 +356,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
connectDevice();
}
- public HearingAidInfo getHearingAidInfo() {
- return mHearingAidInfo;
- }
-
- public void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
+ void setHearingAidInfo(HearingAidInfo hearingAidInfo) {
mHearingAidInfo = hearingAidInfo;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 0c1b793102bf..441d3a52b97f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.util.Log;
@@ -114,10 +115,21 @@ public class CachedBluetoothDeviceManager {
/**
* Create and return a new {@link CachedBluetoothDevice}. This assumes
* that {@link #findDevice} has already been called and returned null.
- * @param device the address of the new Bluetooth device
+ * @param device the new Bluetooth device
* @return the newly created CachedBluetoothDevice object
*/
public CachedBluetoothDevice addDevice(BluetoothDevice device) {
+ return addDevice(device, /*leScanFilters=*/null);
+ }
+
+ /**
+ * Create and return a new {@link CachedBluetoothDevice}. This assumes
+ * that {@link #findDevice} has already been called and returned null.
+ * @param device the new Bluetooth device
+ * @param leScanFilters the BLE scan filters which the device matched
+ * @return the newly created CachedBluetoothDevice object
+ */
+ public CachedBluetoothDevice addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters) {
CachedBluetoothDevice newDevice;
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
synchronized (this) {
@@ -125,7 +137,7 @@ public class CachedBluetoothDeviceManager {
if (newDevice == null) {
newDevice = new CachedBluetoothDevice(mContext, profileManager, device);
mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice);
- mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice);
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice, leScanFilters);
if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice)
&& !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
mCachedDevices.add(newDevice);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index e5e57824f6ef..efba953e3c6b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -18,10 +18,13 @@ package com.android.settingslib.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.le.ScanFilter;
import android.content.ContentResolver;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.audiopolicy.AudioProductStrategy;
+import android.os.ParcelUuid;
import android.provider.Settings;
import android.util.Log;
@@ -59,7 +62,8 @@ public class HearingAidDeviceManager {
mRoutingHelper = routingHelper;
}
- void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) {
+ void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice,
+ List<ScanFilter> leScanFilters) {
long hiSyncId = getHiSyncId(newDevice.getDevice());
if (isValidHiSyncId(hiSyncId)) {
// Once hiSyncId is valid, assign hearing aid info
@@ -68,6 +72,21 @@ public class HearingAidDeviceManager {
.setAshaDeviceMode(getDeviceMode(newDevice.getDevice()))
.setHiSyncId(hiSyncId);
newDevice.setHearingAidInfo(infoBuilder.build());
+ } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) {
+ // If the device is added with hearing aid scan filter during pairing, set an empty
+ // hearing aid info to indicate it's a hearing aid device. The info will be updated
+ // when corresponding profiles connected.
+ for (ScanFilter leScanFilter: leScanFilters) {
+ final ParcelUuid serviceUuid = leScanFilter.getServiceUuid();
+ final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid();
+ if (BluetoothUuid.HEARING_AID.equals(serviceUuid)
+ || BluetoothUuid.HAS.equals(serviceUuid)
+ || BluetoothUuid.HEARING_AID.equals(serviceDataUuid)
+ || BluetoothUuid.HAS.equals(serviceDataUuid)) {
+ newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
+ break;
+ }
+ }
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index f911d35757f6..a617bf3278d6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -28,6 +28,7 @@ import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.UserHandle;
import android.provider.Settings;
import android.service.dreams.DreamService;
import android.service.dreams.IDreamManager;
@@ -35,6 +36,7 @@ import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FrameworkStatsLog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -114,6 +116,26 @@ public class DreamBackend {
private static final int SCREENSAVER_HOME_CONTROLS_ENABLED_DEFAULT = 1;
private static final int LOCKSCREEN_SHOW_CONTROLS_DEFAULT = 0;
+ private static final int DS_TYPE_ENABLED = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__DREAM_SETTING_TYPE__DREAM_SETTING_TYPE_ENABLED;
+ private static final int DS_TYPE_WHEN_TO_DREAM = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__DREAM_SETTING_TYPE__DREAM_SETTING_TYPE_WHEN_TO_DREAM;
+ private static final int DS_TYPE_DREAM_COMPONENT = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__DREAM_SETTING_TYPE__DREAM_SETTING_TYPE_DREAM_COMPONENT;
+ private static final int DS_TYPE_SHOW_ADDITIONAL_INFO = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__DREAM_SETTING_TYPE__DREAM_SETTING_TYPE_SHOW_ADDITIONAL_INFO;
+ private static final int DS_TYPE_SHOW_HOME_CONTROLS = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__DREAM_SETTING_TYPE__DREAM_SETTING_TYPE_SHOW_HOME_CONTROLS;
+
+ private static final int WHEN_TO_DREAM_UNSPECIFIED = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_UNSPECIFIED;
+ private static final int WHEN_TO_DREAM_CHARGING = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY;
+ private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY;
+ private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog
+ .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED;
+
private final Context mContext;
private final IDreamManager mDreamManager;
private final DreamInfoComparator mComparator;
@@ -121,6 +143,7 @@ public class DreamBackend {
private final boolean mDreamsActivatedOnSleepByDefault;
private final boolean mDreamsActivatedOnDockByDefault;
private final Set<ComponentName> mDisabledDreams;
+ private final List<String> mLoggableDreamPrefixes;
private Set<Integer> mSupportedComplications;
private static DreamBackend sInstance;
@@ -148,6 +171,8 @@ public class DreamBackend {
com.android.internal.R.array.config_disabledDreamComponents))
.map(ComponentName::unflattenFromString)
.collect(Collectors.toSet());
+ mLoggableDreamPrefixes = Arrays.stream(resources.getStringArray(
+ com.android.internal.R.array.config_loggable_dream_prefixes)).toList();
mSupportedComplications = Arrays.stream(resources.getIntArray(
com.android.internal.R.array.config_supportedDreamComplications))
@@ -282,6 +307,8 @@ public class DreamBackend {
default:
break;
}
+
+ logDreamSettingChangeToStatsd(DS_TYPE_WHEN_TO_DREAM);
}
/** Gets all complications which have been enabled by the user. */
@@ -304,12 +331,14 @@ public class DreamBackend {
public void setComplicationsEnabled(boolean enabled) {
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.SCREENSAVER_COMPLICATIONS_ENABLED, enabled ? 1 : 0);
+ logDreamSettingChangeToStatsd(DS_TYPE_SHOW_ADDITIONAL_INFO);
}
/** Sets whether home controls are enabled by the user on the dream */
public void setHomeControlsEnabled(boolean enabled) {
Settings.Secure.putInt(mContext.getContentResolver(),
Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, enabled ? 1 : 0);
+ logDreamSettingChangeToStatsd(DS_TYPE_SHOW_HOME_CONTROLS);
}
/** Gets whether home controls button is enabled on the dream */
@@ -353,6 +382,7 @@ public class DreamBackend {
public void setEnabled(boolean value) {
logd("setEnabled(%s)", value);
setBoolean(Settings.Secure.SCREENSAVER_ENABLED, value);
+ logDreamSettingChangeToStatsd(DS_TYPE_ENABLED);
}
public boolean isActivatedOnDock() {
@@ -391,6 +421,7 @@ public class DreamBackend {
try {
ComponentName[] dreams = {dream};
mDreamManager.setDreamComponents(dream == null ? null : dreams);
+ logDreamSettingChangeToStatsd(DS_TYPE_DREAM_COMPONENT);
} catch (RemoteException e) {
Log.w(TAG, "Failed to set active dream to " + dream, e);
}
@@ -461,6 +492,68 @@ public class DreamBackend {
}
}
+ private void logDreamSettingChangeToStatsd(int dreamSettingType) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.DREAM_SETTING_CHANGED, /*atom_tag*/
+ UserHandle.myUserId(), /*uid*/
+ isEnabled(), /*enabled*/
+ getActiveDreamComponentForStatsd(), /*dream_component*/
+ getWhenToDreamForStatsd(), /*when_to_dream*/
+ getComplicationsEnabled(), /*show_additional_info*/
+ getHomeControlsEnabled(), /*show_home_controls*/
+ dreamSettingType /*dream_setting_type*/
+ );
+ }
+
+ /**
+ * Returns the user selected dream component in string format for stats logging. If the dream
+ * component is not loggable, returns "other".
+ */
+ private String getActiveDreamComponentForStatsd() {
+ final ComponentName activeDream = getActiveDream();
+ if (activeDream == null) {
+ return "";
+ }
+
+ final String component = activeDream.flattenToShortString();
+ if (isLoggableDreamComponentForStatsd(component)) {
+ return component;
+ } else {
+ return "other";
+ }
+ }
+
+ /**
+ * Whether the dream component is loggable. Only components from the predefined packages are
+ * allowed to be logged for privacy.
+ */
+ private boolean isLoggableDreamComponentForStatsd(String component) {
+ for (int i = 0; i < mLoggableDreamPrefixes.size(); i++) {
+ if (component.startsWith(mLoggableDreamPrefixes.get(i))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the enum of "when to dream" setting for statsd logging.
+ */
+ private int getWhenToDreamForStatsd() {
+ switch (getWhenToDreamSetting()) {
+ case WHILE_CHARGING:
+ return WHEN_TO_DREAM_CHARGING;
+ case WHILE_DOCKED:
+ return WHEN_TO_DREAM_DOCKED;
+ case EITHER:
+ return WHEN_TO_DREAM_CHARGING_OR_DOCKED;
+ case NEVER:
+ default:
+ return WHEN_TO_DREAM_UNSPECIFIED;
+ }
+ }
+
private static class DreamInfoComparator implements Comparator<DreamInfo> {
private final ComponentName mDefaultDream;
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
index afab0469d114..b9a464752824 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
@@ -27,6 +27,7 @@ import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
import android.net.wifi.WifiInfo;
+import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
@@ -331,6 +332,22 @@ public class WifiUtils {
}
/**
+ * Returns the Hotspot network icon resource.
+ *
+ * @param deviceType The device type of Hotspot network
+ */
+ public static int getHotspotIconResource(int deviceType) {
+ return switch (deviceType) {
+ case NetworkProviderInfo.DEVICE_TYPE_PHONE -> R.drawable.ic_hotspot_phone;
+ case NetworkProviderInfo.DEVICE_TYPE_TABLET -> R.drawable.ic_hotspot_tablet;
+ case NetworkProviderInfo.DEVICE_TYPE_LAPTOP -> R.drawable.ic_hotspot_laptop;
+ case NetworkProviderInfo.DEVICE_TYPE_WATCH -> R.drawable.ic_hotspot_watch;
+ case NetworkProviderInfo.DEVICE_TYPE_AUTO -> R.drawable.ic_hotspot_auto;
+ default -> R.drawable.ic_hotspot_phone; // Return phone icon as default.
+ };
+ }
+
+ /**
* Wrapper the {@link #getInternetIconResource} for testing compatibility.
*/
public static class InternetIconInjector {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 0d5de88cc394..56c8c5a8e549 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -33,6 +33,8 @@ import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
@@ -147,7 +149,7 @@ public class HearingAidDeviceManagerTest {
HearingAidProfile.DeviceSide.SIDE_RIGHT);
assertThat(mCachedDevice1.getHiSyncId()).isNotEqualTo(HISYNCID1);
- mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1);
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1);
assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
@@ -164,12 +166,43 @@ public class HearingAidDeviceManagerTest {
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
BluetoothHearingAid.HI_SYNC_ID_INVALID);
- mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1);
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class));
}
/**
+ * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and hearing aid scan filter, set an
+ * empty hearing aid info on the device.
+ */
+ @Test
+ public void initHearingAidDeviceIfNeeded_hearingAidScanFilter_setHearingAidInfo() {
+ when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
+ BluetoothHearingAid.HI_SYNC_ID_INVALID);
+ final ScanFilter scanFilter = new ScanFilter.Builder()
+ .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}).build();
+
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter));
+
+ assertThat(mCachedDevice1.isHearingAidDevice()).isTrue();
+ }
+
+ /**
+ * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and random scan filter, not to set
+ * hearing aid info on the device.
+ */
+ @Test
+ public void initHearingAidDeviceIfNeeded_randomScanFilter_notToSetHearingAidInfo() {
+ when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
+ BluetoothHearingAid.HI_SYNC_ID_INVALID);
+ final ScanFilter scanFilter = new ScanFilter.Builder().build();
+
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter));
+
+ assertThat(mCachedDevice1.isHearingAidDevice()).isFalse();
+ }
+
+ /**
* Test setSubDeviceIfNeeded, a device with same HiSyncId will be set as sub device
*/
@Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
index 2edf403e5c00..00ae96cfab50 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/dream/DreamBackendTest.java
@@ -75,6 +75,9 @@ public final class DreamBackendTest {
when(res.getStringArray(
com.android.internal.R.array.config_disabledDreamComponents)).thenReturn(
new String[]{});
+ when(res.getStringArray(
+ com.android.internal.R.array.config_loggable_dream_prefixes)).thenReturn(
+ new String[]{});
mBackend = new DreamBackend(mContext);
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
index b60dc6ae8de1..529301138da3 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
@@ -35,6 +35,7 @@ import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkScoreCache;
+import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
@@ -201,6 +202,25 @@ public class WifiUtilsTest {
}
@Test
+ public void getHotspotIconResource_deviceTypeUnknown_shouldNotCrash() {
+ WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_UNKNOWN);
+ }
+
+ @Test
+ public void getHotspotIconResource_deviceTypeExists_shouldNotNull() {
+ assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_PHONE))
+ .isNotNull();
+ assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_TABLET))
+ .isNotNull();
+ assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_LAPTOP))
+ .isNotNull();
+ assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_WATCH))
+ .isNotNull();
+ assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_AUTO))
+ .isNotNull();
+ }
+
+ @Test
public void testInternetIconInjector_getIcon_returnsCorrectValues() {
WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext);
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 56e0643b1e20..ee9883b0b0af 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -320,6 +320,7 @@
<uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
+ <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
<uses-permission android:name="android.permission.SUSPEND_APPS" />
<uses-permission android:name="android.permission.OBSERVE_APP_USAGE" />
<uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" />
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 7be60431b91b..e2599a3583c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -228,6 +228,17 @@ filegroup {
}
filegroup {
+ name: "SystemUI-test-fakes",
+ srcs: [
+ /* Status bar fakes */
+ "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt",
+ ],
+ path: "tests/src",
+}
+
+filegroup {
name: "SystemUI-tests-robolectric-pilots",
srcs: [
/* Keyguard converted tests */
@@ -291,6 +302,11 @@ filegroup {
"tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt",
"tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt",
"tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt",
+
+ /* Status bar wifi converted tests */
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt",
+ "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt",
],
path: "tests/src",
}
@@ -449,6 +465,7 @@ android_robolectric_test {
"tests/robolectric/src/**/*.kt",
"tests/robolectric/src/**/*.java",
":SystemUI-tests-utils",
+ ":SystemUI-test-fakes",
":SystemUI-tests-robolectric-pilots",
],
static_libs: [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index dbfa192f5ec4..37b1ee543e46 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -32,11 +32,10 @@ import android.view.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-import android.widget.FrameLayout
import com.android.app.animation.Interpolators
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CujType
-import com.android.systemui.animation.view.LaunchableFrameLayout
+import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked
import kotlin.math.roundToInt
@@ -622,96 +621,12 @@ private class AnimatedDialog(
viewGroupWithBackground
} else {
- // We will make the dialog window (and therefore its DecorView) fullscreen to make
- // it possible to animate outside its bounds.
- //
- // Before that, we add a new View as a child of the DecorView with the same size and
- // gravity as that DecorView, then we add all original children of the DecorView to
- // that new View. Finally we remove the background of the DecorView and add it to
- // the new View, then we make the DecorView fullscreen. This new View now acts as a
- // fake (non fullscreen) window.
- //
- // On top of that, we also add a fullscreen transparent background between the
- // DecorView and the view that we added so that we can dismiss the dialog when this
- // view is clicked. This is necessary because DecorView overrides onTouchEvent and
- // therefore we can't set the click listener directly on the (now fullscreen)
- // DecorView.
- val fullscreenTransparentBackground = FrameLayout(dialog.context)
- decorView.addView(
- fullscreenTransparentBackground,
- 0 /* index */,
- FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- )
-
- val dialogContentWithBackground = LaunchableFrameLayout(dialog.context)
- dialogContentWithBackground.background = decorView.background
-
- // Make the window background transparent. Note that setting the window (or
- // DecorView) background drawable to null leads to issues with background color (not
- // being transparent) or with insets that are not refreshed. Therefore we need to
- // set it to something not null, hence we are using android.R.color.transparent
- // here.
- window.setBackgroundDrawableResource(android.R.color.transparent)
-
- // Close the dialog when clicking outside of it.
- fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() }
- dialogContentWithBackground.isClickable = true
-
- // Make sure the transparent and dialog backgrounds are not focusable by
- // accessibility
- // features.
- fullscreenTransparentBackground.importantForAccessibility =
- View.IMPORTANT_FOR_ACCESSIBILITY_NO
- dialogContentWithBackground.importantForAccessibility =
- View.IMPORTANT_FOR_ACCESSIBILITY_NO
-
- fullscreenTransparentBackground.addView(
- dialogContentWithBackground,
- FrameLayout.LayoutParams(
- window.attributes.width,
- window.attributes.height,
- window.attributes.gravity
- )
- )
-
- // Move all original children of the DecorView to the new View we just added.
- for (i in 1 until decorView.childCount) {
- val view = decorView.getChildAt(1)
- decorView.removeViewAt(1)
- dialogContentWithBackground.addView(view)
- }
-
- // Make the window fullscreen and add a layout listener to ensure it stays
- // fullscreen.
- window.setLayout(MATCH_PARENT, MATCH_PARENT)
- decorViewLayoutListener =
- View.OnLayoutChangeListener {
- v,
- left,
- top,
- right,
- bottom,
- oldLeft,
- oldTop,
- oldRight,
- oldBottom ->
- if (
- window.attributes.width != MATCH_PARENT ||
- window.attributes.height != MATCH_PARENT
- ) {
- // The dialog size changed, copy its size to dialogContentWithBackground
- // and make the dialog window full screen again.
- val layoutParams = dialogContentWithBackground.layoutParams
- layoutParams.width = window.attributes.width
- layoutParams.height = window.attributes.height
- dialogContentWithBackground.layoutParams = layoutParams
- window.setLayout(MATCH_PARENT, MATCH_PARENT)
- }
- }
- decorView.addOnLayoutChangeListener(decorViewLayoutListener)
-
+ val (dialogContentWithBackground, decorViewLayoutListener) =
+ dialog.maybeForceFullscreen()!!
+ this.decorViewLayoutListener = decorViewLayoutListener
dialogContentWithBackground
}
+
this.dialogContentWithBackground = dialogContentWithBackground
dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index 8e79e3ce1742..38b99cc5f5ee 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -70,6 +70,9 @@ class ViewHierarchyAnimator {
* If a new layout change happens while an animation is already in progress, the animation
* is updated to continue from the current values to the new end state.
*
+ * A set of [excludedViews] can be passed. If any dependent view from [rootView] matches an
+ * entry in this set, changes to that view will not be animated.
+ *
* The animator continues to respond to layout changes until [stopAnimating] is called.
*
* Successive calls to this method override the previous settings ([interpolator] and
@@ -82,9 +85,16 @@ class ViewHierarchyAnimator {
fun animate(
rootView: View,
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
- duration: Long = DEFAULT_DURATION
+ duration: Long = DEFAULT_DURATION,
+ excludedViews: Set<View> = emptySet()
): Boolean {
- return animate(rootView, interpolator, duration, ephemeral = false)
+ return animate(
+ rootView,
+ interpolator,
+ duration,
+ ephemeral = false,
+ excludedViews = excludedViews
+ )
}
/**
@@ -95,16 +105,24 @@ class ViewHierarchyAnimator {
fun animateNextUpdate(
rootView: View,
interpolator: Interpolator = DEFAULT_INTERPOLATOR,
- duration: Long = DEFAULT_DURATION
+ duration: Long = DEFAULT_DURATION,
+ excludedViews: Set<View> = emptySet()
): Boolean {
- return animate(rootView, interpolator, duration, ephemeral = true)
+ return animate(
+ rootView,
+ interpolator,
+ duration,
+ ephemeral = true,
+ excludedViews = excludedViews
+ )
}
private fun animate(
rootView: View,
interpolator: Interpolator,
duration: Long,
- ephemeral: Boolean
+ ephemeral: Boolean,
+ excludedViews: Set<View> = emptySet()
): Boolean {
if (
!occupiesSpace(
@@ -119,7 +137,7 @@ class ViewHierarchyAnimator {
}
val listener = createUpdateListener(interpolator, duration, ephemeral)
- addListener(rootView, listener, recursive = true)
+ addListener(rootView, listener, recursive = true, excludedViews = excludedViews)
return true
}
@@ -921,8 +939,11 @@ class ViewHierarchyAnimator {
private fun addListener(
view: View,
listener: View.OnLayoutChangeListener,
- recursive: Boolean = false
+ recursive: Boolean = false,
+ excludedViews: Set<View> = emptySet()
) {
+ if (excludedViews.contains(view)) return
+
// Make sure that only one listener is active at a time.
val previousListener = view.getTag(R.id.tag_layout_listener)
if (previousListener != null && previousListener is View.OnLayoutChangeListener) {
@@ -933,7 +954,12 @@ class ViewHierarchyAnimator {
view.setTag(R.id.tag_layout_listener, listener)
if (view is ViewGroup && recursive) {
for (i in 0 until view.childCount) {
- addListener(view.getChildAt(i), listener, recursive = true)
+ addListener(
+ view.getChildAt(i),
+ listener,
+ recursive = true,
+ excludedViews = excludedViews
+ )
}
}
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
index 428856dc5f30..0f63548b6f0c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
@@ -18,6 +18,9 @@ package com.android.systemui.util
import android.app.Dialog
import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
import android.window.OnBackInvokedDispatcher
import com.android.systemui.animation.back.BackAnimationSpec
import com.android.systemui.animation.back.BackTransformation
@@ -25,6 +28,7 @@ import com.android.systemui.animation.back.applyTo
import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
import com.android.systemui.animation.back.onBackAnimationCallbackFrom
import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached
+import com.android.systemui.animation.view.LaunchableFrameLayout
/**
* Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec].
@@ -49,3 +53,110 @@ fun Dialog.registerAnimationOnBackInvoked(
),
)
}
+
+/**
+ * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
+ * outside its bounds. No-op if the dialog is already fullscreen.
+ *
+ * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
+ * and a layout listener. The new view matches the original dialog DecorView in size, position, and
+ * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
+ * layout listener is listening to changes to the modified DecorView. It is the responsibility of
+ * the caller to deregister the listener when the dialog is dismissed.
+ */
+fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
+ // Create the dialog so that its onCreate() method is called, which usually sets the dialog
+ // content.
+ create()
+
+ val window = window!!
+ val decorView = window.decorView as ViewGroup
+
+ val isWindowFullscreen =
+ window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT
+ if (isWindowFullscreen) {
+ return null
+ }
+
+ // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible
+ // to animate outside its bounds.
+ //
+ // Before that, we add a new View as a child of the DecorView with the same size and gravity as
+ // that DecorView, then we add all original children of the DecorView to that new View. Finally
+ // we remove the background of the DecorView and add it to the new View, then we make the
+ // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window.
+ //
+ // On top of that, we also add a fullscreen transparent background between the DecorView and the
+ // view that we added so that we can dismiss the dialog when this view is clicked. This is
+ // necessary because DecorView overrides onTouchEvent and therefore we can't set the click
+ // listener directly on the (now fullscreen) DecorView.
+ val fullscreenTransparentBackground = FrameLayout(context)
+ decorView.addView(
+ fullscreenTransparentBackground,
+ 0 /* index */,
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ )
+
+ val dialogContentWithBackground = LaunchableFrameLayout(context)
+ dialogContentWithBackground.background = decorView.background
+
+ // Make the window background transparent. Note that setting the window (or DecorView)
+ // background drawable to null leads to issues with background color (not being transparent) or
+ // with insets that are not refreshed. Therefore we need to set it to something not null, hence
+ // we are using android.R.color.transparent here.
+ window.setBackgroundDrawableResource(android.R.color.transparent)
+
+ // Close the dialog when clicking outside of it.
+ fullscreenTransparentBackground.setOnClickListener { dismiss() }
+ dialogContentWithBackground.isClickable = true
+
+ // Make sure the transparent and dialog backgrounds are not focusable by accessibility
+ // features.
+ fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+
+ fullscreenTransparentBackground.addView(
+ dialogContentWithBackground,
+ FrameLayout.LayoutParams(
+ window.attributes.width,
+ window.attributes.height,
+ window.attributes.gravity
+ )
+ )
+
+ // Move all original children of the DecorView to the new View we just added.
+ for (i in 1 until decorView.childCount) {
+ val view = decorView.getChildAt(1)
+ decorView.removeViewAt(1)
+ dialogContentWithBackground.addView(view)
+ }
+
+ // Make the window fullscreen and add a layout listener to ensure it stays fullscreen.
+ window.setLayout(MATCH_PARENT, MATCH_PARENT)
+ val decorViewLayoutListener =
+ View.OnLayoutChangeListener {
+ v,
+ left,
+ top,
+ right,
+ bottom,
+ oldLeft,
+ oldTop,
+ oldRight,
+ oldBottom ->
+ if (
+ window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT
+ ) {
+ // The dialog size changed, copy its size to dialogContentWithBackground and make
+ // the dialog window full screen again.
+ val layoutParams = dialogContentWithBackground.layoutParams
+ layoutParams.width = window.attributes.width
+ layoutParams.height = window.attributes.height
+ dialogContentWithBackground.layoutParams = layoutParams
+ window.setLayout(MATCH_PARENT, MATCH_PARENT)
+ }
+ }
+ decorView.addOnLayoutChangeListener(decorViewLayoutListener)
+
+ return dialogContentWithBackground to decorViewLayoutListener
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
new file mode 100644
index 000000000000..566967f920d3
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.DisposableEffectResult
+import androidx.compose.runtime.DisposableEffectScope
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.lerp
+import com.android.compose.ui.util.lerp
+
+/**
+ * Animate a shared Int value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedIntAsState(
+ value: Int,
+ key: ValueKey,
+ element: ElementKey,
+ canOverflow: Boolean = true,
+): State<Int> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Float value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedFloatAsState(
+ value: Float,
+ key: ValueKey,
+ element: ElementKey,
+ canOverflow: Boolean = true,
+): State<Float> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Dp value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedDpAsState(
+ value: Dp,
+ key: ValueKey,
+ element: ElementKey,
+ canOverflow: Boolean = true,
+): State<Dp> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Color value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedColorAsState(
+ value: Color,
+ key: ValueKey,
+ element: ElementKey,
+): State<Color> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false)
+}
+
+@Composable
+internal fun <T> animateSharedValueAsState(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ key: ValueKey,
+ value: T,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean,
+): State<T> {
+ val sharedValue = remember(key) { Element.SharedValue(key, value) }
+ if (value != sharedValue.value) {
+ sharedValue.value = value
+ }
+
+ DisposableEffect(element, scene, sharedValue) {
+ addSharedValueToElement(element, scene, sharedValue)
+ }
+
+ return remember(layoutImpl, element, sharedValue, lerp, canOverflow) {
+ derivedStateOf { computeValue(layoutImpl, element, sharedValue, lerp, canOverflow) }
+ }
+}
+
+private fun <T> DisposableEffectScope.addSharedValueToElement(
+ element: Element,
+ scene: Scene,
+ sharedValue: Element.SharedValue<T>,
+): DisposableEffectResult {
+ val sceneValues =
+ element.sceneValues[scene.key] ?: error("Element $element is not present in $scene")
+ val sharedValues = sceneValues.sharedValues
+
+ sharedValues[sharedValue.key] = sharedValue
+ return onDispose { sharedValues.remove(sharedValue.key) }
+}
+
+private fun <T> computeValue(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: Element,
+ sharedValue: Element.SharedValue<T>,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean,
+): T {
+ val state = layoutImpl.state.transitionState
+ if (
+ state !is TransitionState.Transition ||
+ state.fromScene == state.toScene ||
+ !layoutImpl.isTransitionReady(state)
+ ) {
+ return sharedValue.value
+ }
+
+ fun sceneValue(scene: SceneKey): Element.SharedValue<T>? {
+ val sceneValues = element.sceneValues[scene] ?: return null
+ val value = sceneValues.sharedValues[sharedValue.key] ?: return null
+ return value as Element.SharedValue<T>
+ }
+
+ val fromValue = sceneValue(state.fromScene)
+ val toValue = sceneValue(state.toScene)
+ return if (fromValue != null && toValue != null) {
+ val progress = if (canOverflow) state.progress else state.progress.coerceIn(0f, 1f)
+ lerp(fromValue.value, toValue.value, progress)
+ } else if (fromValue != null) {
+ fromValue.value
+ } else if (toValue != null) {
+ toValue.value
+ } else {
+ sharedValue.value
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt
new file mode 100644
index 000000000000..753672820e28
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.SpringSpec
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Transition to [target] using a canned animation. This function will try to be smart and take over
+ * the currently running transition, if there is one.
+ */
+internal fun CoroutineScope.animateToScene(
+ layoutImpl: SceneTransitionLayoutImpl,
+ target: SceneKey,
+) {
+ val state = layoutImpl.state.transitionState
+ if (state.currentScene == target) {
+ // This can happen in 3 different situations, for which there isn't anything else to do:
+ // 1. There is no ongoing transition and [target] is already the current scene.
+ // 2. The user is swiping to [target] from another scene and released their pointer such
+ // that the gesture was committed and the transition is animating to [scene] already.
+ // 3. The user is swiping from [target] to another scene and either:
+ // a. didn't release their pointer yet.
+ // b. released their pointer such that the swipe gesture was cancelled and the
+ // transition is currently animating back to [target].
+ return
+ }
+
+ when (state) {
+ is TransitionState.Idle -> animate(layoutImpl, target)
+ is TransitionState.Transition -> {
+ if (state.toScene == state.fromScene) {
+ // Same as idle.
+ animate(layoutImpl, target)
+ return
+ }
+
+ // A transition is currently running: first check whether `transition.toScene` or
+ // `transition.fromScene` is the same as our target scene, in which case the transition
+ // can be accelerated or reversed to end up in the target state.
+
+ if (state.toScene == target) {
+ // The user is currently swiping to [target] but didn't release their pointer yet:
+ // animate the progress to `1`.
+
+ check(state.fromScene == state.currentScene)
+ val progress = state.progress
+ if ((1f - progress).absoluteValue < ProgressVisibilityThreshold) {
+ // The transition is already finished (progress ~= 1): no need to animate.
+ layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
+ } else {
+ // The transition is in progress: start the canned animation at the same
+ // progress as it was in.
+ // TODO(b/290184746): Also take the current velocity into account.
+ animate(layoutImpl, target, startProgress = progress)
+ }
+
+ return
+ }
+
+ if (state.fromScene == target) {
+ // There is a transition from [target] to another scene: simply animate the same
+ // transition progress to `0`.
+
+ check(state.toScene == state.currentScene)
+ val progress = state.progress
+ if (progress.absoluteValue < ProgressVisibilityThreshold) {
+ // The transition is at progress ~= 0: no need to animate.
+ layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
+ } else {
+ // TODO(b/290184746): Also take the current velocity into account.
+ animate(layoutImpl, target, startProgress = progress, reversed = true)
+ }
+
+ return
+ }
+
+ // Generic interruption; the current transition is neither from or to [target].
+ // TODO(b/290930950): Better handle interruptions here.
+ animate(layoutImpl, target)
+ }
+ }
+}
+
+private fun CoroutineScope.animate(
+ layoutImpl: SceneTransitionLayoutImpl,
+ target: SceneKey,
+ startProgress: Float = 0f,
+ reversed: Boolean = false,
+) {
+ val fromScene = layoutImpl.state.transitionState.currentScene
+
+ val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec
+ val visibilityThreshold =
+ (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
+ val animatable = Animatable(startProgress, visibilityThreshold = visibilityThreshold)
+
+ val targetProgress = if (reversed) 0f else 1f
+ val transition =
+ if (reversed) {
+ OneOffTransition(target, fromScene, currentScene = target, animatable)
+ } else {
+ OneOffTransition(fromScene, target, currentScene = target, animatable)
+ }
+
+ // Change the current layout state to use this new transition.
+ layoutImpl.state.transitionState = transition
+
+ // Animate the progress to its target value.
+ launch {
+ animatable.animateTo(targetProgress, animationSpec)
+
+ // Unless some other external state change happened, the state should now be idle.
+ if (layoutImpl.state.transitionState == transition) {
+ layoutImpl.state.transitionState = TransitionState.Idle(target)
+ }
+ }
+}
+
+private class OneOffTransition(
+ override val fromScene: SceneKey,
+ override val toScene: SceneKey,
+ override val currentScene: SceneKey,
+ private val animatable: Animatable<Float, AnimationVector1D>,
+) : TransitionState.Transition {
+ override val progress: Float
+ get() = animatable.value
+}
+
+// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
+// and screen density.
+private const val ProgressVisibilityThreshold = 1e-3f
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt
new file mode 100644
index 000000000000..0cc259ab7015
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt
@@ -0,0 +1,449 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.IntermediateMeasureScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.util.lerp
+
+/** An element on screen, that can be composed in one or more scenes. */
+internal class Element(val key: ElementKey) {
+ /**
+ * The last offset assigned to this element, relative to the SceneTransitionLayout containing
+ * it.
+ */
+ var lastOffset = Offset.Unspecified
+
+ /** The last size assigned to this element. */
+ var lastSize = SizeUnspecified
+
+ /** The last alpha assigned to this element. */
+ var lastAlpha = 1f
+
+ /** The mapping between a scene and the values/state this element has in that scene, if any. */
+ val sceneValues = SnapshotStateMap<SceneKey, SceneValues>()
+
+ override fun toString(): String {
+ return "Element(key=$key)"
+ }
+
+ /** The target values of this element in a given scene. */
+ class SceneValues {
+ var size by mutableStateOf(SizeUnspecified)
+ var offset by mutableStateOf(Offset.Unspecified)
+ val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
+ }
+
+ /** A shared value of this element. */
+ class SharedValue<T>(val key: ValueKey, initialValue: T) {
+ var value by mutableStateOf(initialValue)
+ }
+
+ companion object {
+ val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
+ }
+}
+
+/** The implementation of [SceneScope.element]. */
+@Composable
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun Modifier.element(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ key: ElementKey,
+): Modifier {
+ val sceneValues = remember(scene, key) { Element.SceneValues() }
+ val element =
+ // Get the element associated to [key] if it was already composed in another scene,
+ // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
+ // withoutReadObservation() because there is no need to recompose when that map is mutated.
+ Snapshot.withoutReadObservation {
+ val element =
+ layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
+ val previousValues = element.sceneValues[scene.key]
+ if (previousValues == null) {
+ element.sceneValues[scene.key] = sceneValues
+ } else if (previousValues != sceneValues) {
+ error("$key was composed multiple times in $scene")
+ }
+
+ element
+ }
+
+ DisposableEffect(scene, sceneValues, element) {
+ onDispose {
+ element.sceneValues.remove(scene.key)
+
+ // This was the last scene this element was in, so remove it from the map.
+ if (element.sceneValues.isEmpty()) {
+ layoutImpl.elements.remove(element.key)
+ }
+ }
+ }
+
+ val alpha =
+ remember(layoutImpl, element, scene) {
+ derivedStateOf { elementAlpha(layoutImpl, element, scene) }
+ }
+ val isOpaque by remember(alpha) { derivedStateOf { alpha.value == 1f } }
+ SideEffect {
+ if (isOpaque && element.lastAlpha != 1f) {
+ element.lastAlpha = 1f
+ }
+ }
+
+ return drawWithContent {
+ if (shouldDrawElement(layoutImpl, scene, element)) {
+ drawContent()
+ }
+ }
+ .modifierTransformations(layoutImpl, scene, element, sceneValues)
+ .intermediateLayout { measurable, constraints ->
+ val placeable =
+ measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
+ layout(placeable.width, placeable.height) {
+ place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this)
+ }
+ }
+ .thenIf(!isOpaque) {
+ Modifier.graphicsLayer {
+ val alpha = alpha.value
+ this.alpha = alpha
+ element.lastAlpha = alpha
+ }
+ }
+ .testTag(key.name)
+}
+
+private fun shouldDrawElement(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+): Boolean {
+ val state = layoutImpl.state.transitionState
+
+ // Always draw the element if there is no ongoing transition or if the element is not shared.
+ if (
+ state !is TransitionState.Transition ||
+ state.fromScene == state.toScene ||
+ !layoutImpl.isTransitionReady(state) ||
+ state.fromScene !in element.sceneValues ||
+ state.toScene !in element.sceneValues
+ ) {
+ return true
+ }
+
+ val otherScene =
+ layoutImpl.scenes.getValue(
+ if (scene.key == state.fromScene) {
+ state.toScene
+ } else {
+ state.fromScene
+ }
+ )
+
+ // When the element is shared, draw the one in the highest scene unless it is a background, i.e.
+ // it is usually drawn below everything else.
+ val isHighestScene = scene.zIndex > otherScene.zIndex
+ return if (element.key.isBackground) {
+ !isHighestScene
+ } else {
+ isHighestScene
+ }
+}
+
+/**
+ * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
+ * throughout the current transition, if any.
+ */
+private fun Modifier.modifierTransformations(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+): Modifier {
+ when (val state = layoutImpl.state.transitionState) {
+ is TransitionState.Idle -> return this
+ is TransitionState.Transition -> {
+ val fromScene = state.fromScene
+ val toScene = state.toScene
+ if (fromScene == toScene) {
+ // Same as idle.
+ return this
+ }
+
+ return layoutImpl.transitions
+ .transitionSpec(fromScene, state.toScene)
+ .transformations(element.key)
+ .modifier
+ .fold(this) { modifier, transformation ->
+ with(transformation) {
+ modifier.transform(layoutImpl, scene, element, sceneValues)
+ }
+ }
+ }
+ }
+}
+
+private fun elementAlpha(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: Element,
+ scene: Scene
+): Float {
+ return computeValue(
+ layoutImpl,
+ scene,
+ element,
+ sceneValue = { 1f },
+ transformation = { it.alpha },
+ idleValue = 1f,
+ currentValue = { 1f },
+ lastValue = { element.lastAlpha },
+ ::lerp,
+ )
+ .coerceIn(0f, 1f)
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun IntermediateMeasureScope.measure(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ measurable: Measurable,
+ constraints: Constraints,
+): Placeable {
+ // Update the size this element has in this scene when idle.
+ val targetSizeInScene = lookaheadSize
+ if (targetSizeInScene != sceneValues.size) {
+ // TODO(b/290930950): Better handle when this changes to avoid instant size jumps.
+ sceneValues.size = targetSizeInScene
+ }
+
+ // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
+ // case we store the resulting placeable here to make sure the element is not measured more than
+ // once.
+ var maybePlaceable: Placeable? = null
+
+ fun Placeable.size() = IntSize(width, height)
+
+ val targetSize =
+ computeValue(
+ layoutImpl,
+ scene,
+ element,
+ sceneValue = { it.size },
+ transformation = { it.size },
+ idleValue = lookaheadSize,
+ currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
+ lastValue = {
+ val lastSize = element.lastSize
+ if (lastSize != Element.SizeUnspecified) {
+ lastSize
+ } else {
+ measurable.measure(constraints).also { maybePlaceable = it }.size()
+ }
+ },
+ ::lerp,
+ )
+
+ val placeable =
+ maybePlaceable
+ ?: measurable.measure(
+ Constraints.fixed(
+ targetSize.width.coerceAtLeast(0),
+ targetSize.height.coerceAtLeast(0),
+ )
+ )
+
+ element.lastSize = placeable.size()
+ return placeable
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun IntermediateMeasureScope.place(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ placeable: Placeable,
+ placementScope: Placeable.PlacementScope,
+) {
+ with(placementScope) {
+ // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
+ // when idle.
+ val coords = coordinates!!
+ val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
+ if (targetOffsetInScene != sceneValues.offset) {
+ // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
+ sceneValues.offset = targetOffsetInScene
+ }
+
+ val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
+ val targetOffset =
+ computeValue(
+ layoutImpl,
+ scene,
+ element,
+ sceneValue = { it.offset },
+ transformation = { it.offset },
+ idleValue = targetOffsetInScene,
+ currentValue = { currentOffset },
+ lastValue = {
+ val lastValue = element.lastOffset
+ if (lastValue.isSpecified) {
+ lastValue
+ } else {
+ currentOffset
+ }
+ },
+ ::lerp,
+ )
+
+ element.lastOffset = targetOffset
+ placeable.place((targetOffset - currentOffset).round())
+ }
+}
+
+/**
+ * Return the value that should be used depending on the current layout state and transition.
+ *
+ * Important: This function must remain inline because of all the lambda parameters. These lambdas
+ * are necessary because getting some of them might require some computation, like measuring a
+ * Measurable.
+ *
+ * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
+ * @param scene the scene containing [element].
+ * @param element the element being animated.
+ * @param sceneValue the value being animated.
+ * @param transformation the transformation associated to the value being animated.
+ * @param idleValue the value when idle, i.e. when there is no transition happening.
+ * @param currentValue the value that would be used if it is not transformed. Note that this is
+ * different than [idleValue] even if the value is not transformed directly because it could be
+ * impacted by the transformations on other elements, like a parent that is being translated or
+ * resized.
+ * @param lastValue the last value that was used. This should be equal to [currentValue] if this is
+ * the first time the value is set.
+ * @param lerp the linear interpolation function used to interpolate between two values of this
+ * value type.
+ */
+private inline fun <T> computeValue(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValue: (Element.SceneValues) -> T,
+ transformation: (ElementTransformations) -> PropertyTransformation<T>?,
+ idleValue: T,
+ currentValue: () -> T,
+ lastValue: () -> T,
+ lerp: (T, T, Float) -> T,
+): T {
+ val state = layoutImpl.state.transitionState
+
+ // There is no ongoing transition.
+ if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
+ return idleValue
+ }
+
+ // A transition was started but it's not ready yet (not all elements have been composed/laid
+ // out yet). Use the last value that was set, to make sure elements don't unexpectedly jump.
+ if (!layoutImpl.isTransitionReady(state)) {
+ return lastValue()
+ }
+
+ val fromScene = state.fromScene
+ val toScene = state.toScene
+ val fromValues = element.sceneValues[fromScene]
+ val toValues = element.sceneValues[toScene]
+
+ if (fromValues == null && toValues == null) {
+ error("This should not happen, element $element is neither in $fromScene or $toScene")
+ }
+
+ // TODO(b/291053278): Handle overscroll correctly. We should probably coerce between [0f, 1f]
+ // here and consume overflows at drawing time, somehow reusing Compose OverflowEffect or some
+ // similar mechanism.
+ val transitionProgress = state.progress
+
+ // The element is shared: interpolate between the value in fromScene and the value in toScene.
+ // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
+ // elements follow the finger direction.
+ if (fromValues != null && toValues != null) {
+ return lerp(
+ sceneValue(fromValues),
+ sceneValue(toValues),
+ transitionProgress,
+ )
+ }
+
+ val transformation =
+ transformation(
+ layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key)
+ )
+ // If there is no transformation explicitly associated to this element value, let's use
+ // the value given by the system (like the current position and size given by the layout
+ // pass).
+ ?: return currentValue()
+
+ // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
+ // end (for leaving elements) of the transition.
+ val targetValue =
+ transformation.transform(
+ layoutImpl,
+ scene,
+ element,
+ fromValues ?: toValues!!,
+ state,
+ idleValue,
+ )
+
+ // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
+ val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress
+
+ // Interpolate between the value at rest and the value before entering/after leaving.
+ val isEntering = fromValues == null
+ return if (isEntering) {
+ lerp(targetValue, idleValue, rangeProgress)
+ } else {
+ lerp(idleValue, targetValue, rangeProgress)
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
new file mode 100644
index 000000000000..c3f44f8b1069
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+/**
+ * A base class to create unique keys, associated to an [identity] that is used to check the
+ * equality of two key instances.
+ */
+sealed class Key(val name: String, val identity: Any) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (this.javaClass != other?.javaClass) return false
+ return identity == (other as? Key)?.identity
+ }
+
+ override fun hashCode(): Int {
+ return identity.hashCode()
+ }
+
+ override fun toString(): String {
+ return "Key(name=$name)"
+ }
+}
+
+/** Key for a scene. */
+class SceneKey(name: String, identity: Any = Object()) : Key(name, identity) {
+ override fun toString(): String {
+ return "SceneKey(name=$name)"
+ }
+}
+
+/** Key for an element. */
+class ElementKey(
+ name: String,
+ identity: Any = Object(),
+
+ /**
+ * Whether this element is a background and usually drawn below other elements. This should be
+ * set to true to make sure that shared backgrounds are drawn below elements of other scenes.
+ */
+ val isBackground: Boolean = false,
+) : Key(name, identity), ElementMatcher {
+ override fun matches(key: ElementKey): Boolean {
+ return key == this
+ }
+
+ override fun toString(): String {
+ return "ElementKey(name=$name)"
+ }
+
+ companion object {
+ /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
+ fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
+ return object : ElementMatcher {
+ override fun matches(key: ElementKey): Boolean = predicate(key.identity)
+ }
+ }
+ }
+}
+
+/** Key for a shared value of an element. */
+class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
+ override fun toString(): String {
+ return "ValueKey(name=$name)"
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt
new file mode 100644
index 000000000000..a625250d1e51
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.snapshotFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * A scene transition state.
+ *
+ * This models the same thing as [TransitionState], with the following distinctions:
+ * 1. [TransitionState] values are backed by the Snapshot system (Compose State objects) and can be
+ * used by callers tracking State reads, for instance in Compose code during the composition,
+ * layout or Compose drawing phases.
+ * 2. [ObservableTransitionState] values are backed by Kotlin [Flow]s and can be collected by
+ * non-Compose code to observe value changes.
+ * 3. [ObservableTransitionState.Transition.fromScene] and
+ * [ObservableTransitionState.Transition.toScene] will never be equal, while
+ * [TransitionState.Transition.fromScene] and [TransitionState.Transition.toScene] can be equal.
+ */
+sealed class ObservableTransitionState {
+ /** No transition/animation is currently running. */
+ data class Idle(val scene: SceneKey) : ObservableTransitionState()
+
+ /** There is a transition animating between two scenes. */
+ data class Transition(
+ val fromScene: SceneKey,
+ val toScene: SceneKey,
+ val progress: Flow<Float>,
+ ) : ObservableTransitionState()
+}
+
+/**
+ * The current [ObservableTransitionState]. This models the same thing as
+ * [SceneTransitionLayoutState.transitionState], except that it is backed by Flows and can be used
+ * by non-Compose code to observe state changes.
+ */
+fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTransitionState> {
+ return snapshotFlow {
+ when (val state = transitionState) {
+ is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene)
+ is TransitionState.Transition -> {
+ if (state.fromScene == state.toScene) {
+ ObservableTransitionState.Idle(state.currentScene)
+ } else {
+ ObservableTransitionState.Transition(
+ fromScene = state.fromScene,
+ toScene = state.toScene,
+ progress = snapshotFlow { state.progress },
+ )
+ }
+ }
+ }
+ }
+ .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt
new file mode 100644
index 000000000000..b44c8efc7ee2
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.zIndex
+
+/** A scene in a [SceneTransitionLayout]. */
+internal class Scene(
+ val key: SceneKey,
+ layoutImpl: SceneTransitionLayoutImpl,
+ content: @Composable SceneScope.() -> Unit,
+ actions: Map<UserAction, SceneKey>,
+ zIndex: Float,
+) {
+ private val scope = SceneScopeImpl(layoutImpl, this)
+
+ var content by mutableStateOf(content)
+ var userActions by mutableStateOf(actions)
+ var zIndex by mutableFloatStateOf(zIndex)
+ var size by mutableStateOf(IntSize.Zero)
+
+ @Composable
+ fun Content(modifier: Modifier = Modifier) {
+ Box(modifier.zIndex(zIndex).onPlaced { size = it.size }) { scope.content() }
+ }
+
+ override fun toString(): String {
+ return "Scene(key=$key)"
+ }
+}
+
+private class SceneScopeImpl(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+ private val scene: Scene,
+) : SceneScope {
+ @Composable
+ override fun Modifier.element(key: ElementKey): Modifier {
+ return element(layoutImpl, scene, key)
+ }
+
+ @Composable
+ override fun <T> animateSharedValueAsState(
+ value: T,
+ key: ValueKey,
+ element: ElementKey,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean
+ ): State<T> {
+ val element =
+ layoutImpl.elements[element]
+ ?: error(
+ "Element $element is not composed. Make sure to call animateSharedXAsState " +
+ "*after* Modifier.element(key)."
+ )
+
+ return animateSharedValueAsState(
+ layoutImpl,
+ scene,
+ element,
+ key,
+ value,
+ lerp,
+ canOverflow,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
new file mode 100644
index 000000000000..39173d98538f
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+
+/**
+ * [SceneTransitionLayout] is a container that automatically animates its content whenever
+ * [currentScene] changes, using the transitions defined in [transitions].
+ *
+ * Note: You should use [androidx.compose.animation.AnimatedContent] instead of
+ * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if
+ * you need support for swipe gestures, shared elements or transitions defined declaratively outside
+ * UI code.
+ *
+ * @param currentScene the current scene
+ * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
+ * This is called when the user commits a transition to a new scene because of a [UserAction], for
+ * instance by triggering back navigation or by swiping to a new scene.
+ * @param transitions the definition of the transitions used to animate a change of scene.
+ * @param state the observable state of this layout.
+ * @param scenes the configuration of the different scenes of this layout.
+ */
+@Composable
+fun SceneTransitionLayout(
+ currentScene: SceneKey,
+ onChangeScene: (SceneKey) -> Unit,
+ transitions: SceneTransitions,
+ modifier: Modifier = Modifier,
+ state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+ scenes: SceneTransitionLayoutScope.() -> Unit,
+) {
+ val density = LocalDensity.current
+ val layoutImpl = remember {
+ SceneTransitionLayoutImpl(
+ onChangeScene,
+ scenes,
+ transitions,
+ state,
+ density,
+ )
+ }
+
+ layoutImpl.onChangeScene = onChangeScene
+ layoutImpl.transitions = transitions
+ layoutImpl.density = density
+ layoutImpl.setScenes(scenes)
+ layoutImpl.setCurrentScene(currentScene)
+
+ layoutImpl.Content(modifier)
+}
+
+interface SceneTransitionLayoutScope {
+ /**
+ * Add a scene to this layout, identified by [key].
+ *
+ * You can configure [userActions] so that swiping on this layout or navigating back will
+ * transition to a different scene.
+ *
+ * Important: scene order along the z-axis follows call order. Calling scene(A) followed by
+ * scene(B) will mean that scene B renders after/above scene A.
+ */
+ fun scene(
+ key: SceneKey,
+ userActions: Map<UserAction, SceneKey> = emptyMap(),
+ content: @Composable SceneScope.() -> Unit,
+ )
+}
+
+interface SceneScope {
+ /**
+ * Tag an element identified by [key].
+ *
+ * Tagging an element will allow you to reference that element when defining transitions, so
+ * that the element can be transformed and animated when the scene transitions in or out.
+ *
+ * Additionally, this [key] will be used to detect elements that are shared between scenes to
+ * automatically interpolate their size, offset and [shared values][animateSharedValueAsState].
+ *
+ * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable
+ * constraint.
+ */
+ @Composable fun Modifier.element(key: ElementKey): Modifier
+
+ /**
+ * Animate some value of a shared element.
+ *
+ * @param value the value of this shared value in the current scene.
+ * @param key the key of this shared value.
+ * @param element the element associated with this value.
+ * @param lerp the *linear* interpolation function that should be used to interpolate between
+ * two different values. Note that it has to be linear because the [fraction] passed to this
+ * interpolator is already interpolated.
+ * @param canOverflow whether this value can overflow past the values it is interpolated
+ * between, for instance because the transition is animated using a bouncy spring.
+ * @see animateSharedIntAsState
+ * @see animateSharedFloatAsState
+ * @see animateSharedDpAsState
+ * @see animateSharedColorAsState
+ */
+ @Composable
+ fun <T> animateSharedValueAsState(
+ value: T,
+ key: ValueKey,
+ element: ElementKey,
+ lerp: (start: T, stop: T, fraction: Float) -> T,
+ canOverflow: Boolean,
+ ): State<T>
+}
+
+/** An action performed by the user. */
+sealed interface UserAction
+
+/** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
+object Back : UserAction
+
+/** The user swiped on the container. */
+enum class Swipe : UserAction {
+ Up,
+ Down,
+ Left,
+ Right,
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
new file mode 100644
index 000000000000..350b9c2550c8
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.ui.util.fastForEach
+import kotlinx.coroutines.channels.Channel
+
+internal class SceneTransitionLayoutImpl(
+ onChangeScene: (SceneKey) -> Unit,
+ builder: SceneTransitionLayoutScope.() -> Unit,
+ transitions: SceneTransitions,
+ internal val state: SceneTransitionLayoutState,
+ density: Density,
+) {
+ internal val scenes = SnapshotStateMap<SceneKey, Scene>()
+ internal val elements = SnapshotStateMap<ElementKey, Element>()
+
+ /** The scenes that are "ready", i.e. they were composed and fully laid-out at least once. */
+ private val readyScenes = SnapshotStateMap<SceneKey, Boolean>()
+
+ internal var onChangeScene by mutableStateOf(onChangeScene)
+ internal var transitions by mutableStateOf(transitions)
+ internal var density: Density by mutableStateOf(density)
+
+ /**
+ * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
+ * any scene configured or right before the first measure pass of the layout.
+ */
+ internal var size by mutableStateOf(IntSize.Zero)
+
+ init {
+ setScenes(builder)
+ }
+
+ internal fun scene(key: SceneKey): Scene {
+ return scenes[key] ?: error("Scene $key is not configured")
+ }
+
+ internal fun setScenes(builder: SceneTransitionLayoutScope.() -> Unit) {
+ // Keep a reference of the current scenes. After processing [builder], the scenes that were
+ // not configured will be removed.
+ val scenesToRemove = scenes.keys.toMutableSet()
+
+ // The incrementing zIndex of each scene.
+ var zIndex = 0f
+
+ object : SceneTransitionLayoutScope {
+ override fun scene(
+ key: SceneKey,
+ userActions: Map<UserAction, SceneKey>,
+ content: @Composable SceneScope.() -> Unit,
+ ) {
+ scenesToRemove.remove(key)
+
+ val scene = scenes[key]
+ if (scene != null) {
+ // Update an existing scene.
+ scene.content = content
+ scene.userActions = userActions
+ scene.zIndex = zIndex
+ } else {
+ // New scene.
+ scenes[key] =
+ Scene(
+ key,
+ this@SceneTransitionLayoutImpl,
+ content,
+ userActions,
+ zIndex,
+ )
+ }
+
+ zIndex++
+ }
+ }
+ .builder()
+
+ scenesToRemove.forEach { scenes.remove(it) }
+ }
+
+ @Composable
+ internal fun setCurrentScene(key: SceneKey) {
+ val channel = remember { Channel<SceneKey>(Channel.CONFLATED) }
+ SideEffect { channel.trySend(key) }
+ LaunchedEffect(channel) {
+ for (newKey in channel) {
+ // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
+ // late.
+ val newKey = channel.tryReceive().getOrNull() ?: newKey
+ animateToScene(this@SceneTransitionLayoutImpl, newKey)
+ }
+ }
+ }
+
+ @Composable
+ @OptIn(ExperimentalComposeUiApi::class)
+ internal fun Content(modifier: Modifier) {
+ Box(
+ modifier
+ // Handle horizontal and vertical swipes on this layout.
+ // Note: order here is important and will give a slight priority to the vertical
+ // swipes.
+ .swipeToScene(layoutImpl = this, Orientation.Horizontal)
+ .swipeToScene(layoutImpl = this, Orientation.Vertical)
+ .onSizeChanged { size = it }
+ ) {
+ LookaheadScope {
+ val scenesToCompose =
+ when (val state = state.transitionState) {
+ is TransitionState.Idle -> listOf(scene(state.currentScene))
+ is TransitionState.Transition -> {
+ if (state.toScene != state.fromScene) {
+ listOf(scene(state.toScene), scene(state.fromScene))
+ } else {
+ listOf(scene(state.fromScene))
+ }
+ }
+ }
+
+ // Handle back events.
+ // TODO(b/290184746): Make sure that this works with SystemUI once we use
+ // SceneTransitionLayout in Flexiglass.
+ scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
+ BackHandler { onChangeScene(backScene) }
+ }
+
+ Box(
+ Modifier.drawWithContent {
+ drawContent()
+
+ // At this point, all scenes in scenesToCompose are fully laid out so they
+ // are marked as ready. This is necessary because the animation code needs
+ // to know the position and size of the elements in each scenes they are in,
+ // so [readyScenes] will be used to decide whether the transition is ready
+ // (see isTransitionReady() below).
+ //
+ // We can't do that in a DisposableEffect or SideEffect because those are
+ // run between composition and layout. LaunchedEffect could work and might
+ // be better, but it looks like launched effects run a frame later than this
+ // code so doing this here seems better for performance.
+ scenesToCompose.fastForEach { readyScenes[it.key] = true }
+ }
+ ) {
+ scenesToCompose.fastForEach { scene ->
+ val key = scene.key
+ key(key) {
+ DisposableEffect(key) { onDispose { readyScenes.remove(key) } }
+
+ scene.Content(
+ Modifier.drawWithContent {
+ when (val state = state.transitionState) {
+ is TransitionState.Idle -> drawContent()
+ is TransitionState.Transition -> {
+ // Don't draw scenes that are not ready yet.
+ if (
+ readyScenes.containsKey(key) ||
+ state.fromScene == state.toScene
+ ) {
+ drawContent()
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Return whether [transition] is ready, i.e. the elements of both scenes of the transition were
+ * laid out at least once.
+ */
+ internal fun isTransitionReady(transition: TransitionState.Transition): Boolean {
+ return readyScenes.containsKey(transition.fromScene) &&
+ readyScenes.containsKey(transition.toScene)
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
new file mode 100644
index 000000000000..47e3d5add27b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/** The state of a [SceneTransitionLayout]. */
+class SceneTransitionLayoutState(initialScene: SceneKey) {
+ /**
+ * The current [TransitionState]. All values read here are backed by the Snapshot system.
+ *
+ * To observe those values outside of Compose/the Snapshot system, use
+ * [SceneTransitionLayoutState.observableTransitionState] instead.
+ */
+ var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene))
+ internal set
+}
+
+sealed interface TransitionState {
+ /**
+ * The current effective scene. If a new transition was triggered, it would start from this
+ * scene.
+ *
+ * For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe
+ * gesture starts, but then if the user flings their finger and commits the transition to scene
+ * B, then [currentScene] becomes scene B even if the transition is not finished yet and is
+ * still animating to settle to scene B.
+ */
+ val currentScene: SceneKey
+
+ /** No transition/animation is currently running. */
+ data class Idle(override val currentScene: SceneKey) : TransitionState
+
+ /**
+ * There is a transition animating between two scenes.
+ *
+ * Important note: [fromScene] and [toScene] might be the same, in which case this [Transition]
+ * should be treated the same as [Idle]. This is designed on purpose so that a [Transition] can
+ * be started without knowing in advance where it is transitioning to, making the logic of
+ * [swipeToScene] easier to reason about.
+ */
+ interface Transition : TransitionState {
+ /** The scene this transition is starting from. */
+ val fromScene: SceneKey
+
+ /** The scene this transition is going to. */
+ val toScene: SceneKey
+
+ /**
+ * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be
+ * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or
+ * when flinging quickly during a swipe gesture.
+ */
+ val progress: Float
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
new file mode 100644
index 000000000000..9752f53fbd49
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.snap
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.transformation.AnchoredSize
+import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.EdgeTranslate
+import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.ModifierTransformation
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
+import com.android.compose.animation.scene.transformation.ScaleSize
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.Translate
+import com.android.compose.ui.util.fastForEach
+import com.android.compose.ui.util.fastMap
+
+/** The transitions configuration of a [SceneTransitionLayout]. */
+class SceneTransitions(
+ val transitionSpecs: List<TransitionSpec>,
+) {
+ private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
+
+ internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+ return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
+ }
+
+ private fun findSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+ val spec = transition(from, to) { it.from == from && it.to == to }
+ if (spec != null) {
+ return spec
+ }
+
+ val reversed = transition(from, to) { it.from == to && it.to == from }
+ if (reversed != null) {
+ return reversed.reverse()
+ }
+
+ val relaxedSpec =
+ transition(from, to) {
+ (it.from == from && it.to == null) || (it.to == to && it.from == null)
+ }
+ if (relaxedSpec != null) {
+ return relaxedSpec
+ }
+
+ return transition(from, to) {
+ (it.from == to && it.to == null) || (it.to == from && it.from == null)
+ }
+ ?.reverse()
+ ?: defaultTransition(from, to)
+ }
+
+ private fun transition(
+ from: SceneKey,
+ to: SceneKey,
+ filter: (TransitionSpec) -> Boolean,
+ ): TransitionSpec? {
+ var match: TransitionSpec? = null
+ transitionSpecs.fastForEach { spec ->
+ if (filter(spec)) {
+ if (match != null) {
+ error("Found multiple transition specs for transition $from => $to")
+ }
+ match = spec
+ }
+ }
+ return match
+ }
+
+ private fun defaultTransition(from: SceneKey, to: SceneKey) =
+ TransitionSpec(from, to, emptyList(), snap())
+}
+
+/** The definition of a transition between [from] and [to]. */
+data class TransitionSpec(
+ val from: SceneKey?,
+ val to: SceneKey?,
+ val transformations: List<Transformation>,
+ val spec: AnimationSpec<Float>,
+) {
+ private val cache = mutableMapOf<ElementKey, ElementTransformations>()
+
+ internal fun reverse(): TransitionSpec {
+ return copy(
+ from = to,
+ to = from,
+ transformations = transformations.fastMap { it.reverse() },
+ )
+ }
+
+ internal fun transformations(element: ElementKey): ElementTransformations {
+ return cache.getOrPut(element) { computeTransformations(element) }
+ }
+
+ /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
+ private fun computeTransformations(element: ElementKey): ElementTransformations {
+ val modifier = mutableListOf<ModifierTransformation>()
+ var offset: PropertyTransformation<Offset>? = null
+ var size: PropertyTransformation<IntSize>? = null
+ var alpha: PropertyTransformation<Float>? = null
+
+ fun <T> onPropertyTransformation(
+ root: PropertyTransformation<T>,
+ current: PropertyTransformation<T> = root,
+ ) {
+ when (current) {
+ is Translate,
+ is EdgeTranslate,
+ is AnchoredTranslate -> {
+ throwIfNotNull(offset, element, property = "offset")
+ offset = root as PropertyTransformation<Offset>
+ }
+ is ScaleSize,
+ is AnchoredSize -> {
+ throwIfNotNull(size, element, property = "size")
+ size = root as PropertyTransformation<IntSize>
+ }
+ is Fade -> {
+ throwIfNotNull(alpha, element, property = "alpha")
+ alpha = root as PropertyTransformation<Float>
+ }
+ is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
+ }
+ }
+
+ transformations.fastForEach { transformation ->
+ if (!transformation.matcher.matches(element)) {
+ return@fastForEach
+ }
+
+ when (transformation) {
+ is ModifierTransformation -> modifier.add(transformation)
+ is PropertyTransformation<*> -> onPropertyTransformation(transformation)
+ }
+ }
+
+ return ElementTransformations(modifier, offset, size, alpha)
+ }
+
+ private fun throwIfNotNull(
+ previous: PropertyTransformation<*>?,
+ element: ElementKey,
+ property: String,
+ ) {
+ if (previous != null) {
+ error("$element has multiple transformations for its $property property")
+ }
+ }
+}
+
+/** The transformations of an element during a transition. */
+internal class ElementTransformations(
+ val modifier: List<ModifierTransformation>,
+ val offset: PropertyTransformation<Offset>?,
+ val size: PropertyTransformation<IntSize>?,
+ val alpha: PropertyTransformation<Float>?,
+)
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt
new file mode 100644
index 000000000000..d9a45cd663c2
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
+ */
+@Composable
+internal fun Modifier.swipeToScene(
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+): Modifier {
+ val state = layoutImpl.state.transitionState
+ val currentScene = layoutImpl.scene(state.currentScene)
+ val transition = remember {
+ // Note that the currentScene here does not matter, it's only used for initializing the
+ // transition and will be replaced when a drag event starts.
+ SwipeTransition(initialScene = currentScene)
+ }
+
+ val enabled = state == transition || currentScene.shouldEnableSwipes(orientation)
+
+ // Immediately start the drag if this our [transition] is currently animating to a scene (i.e.
+ // the user released their input pointer after swiping in this orientation) and the user can't
+ // swipe in the other direction.
+ val startDragImmediately =
+ state == transition &&
+ transition.isAnimatingOffset &&
+ !currentScene.shouldEnableSwipes(orientation.opposite())
+
+ // The velocity threshold at which the intent of the user is to swipe up or down. It is the same
+ // as SwipeableV2Defaults.VelocityThreshold.
+ val velocityThreshold = with(LocalDensity.current) { 125.dp.toPx() }
+
+ // The positional threshold at which the intent of the user is to swipe to the next scene. It is
+ // the same as SwipeableV2Defaults.PositionalThreshold.
+ val positionalThreshold = with(LocalDensity.current) { 56.dp.toPx() }
+
+ return draggable(
+ orientation = orientation,
+ enabled = enabled,
+ startDragImmediately = startDragImmediately,
+ onDragStarted = { onDragStarted(layoutImpl, transition, orientation) },
+ state =
+ rememberDraggableState { delta -> onDrag(layoutImpl, transition, orientation, delta) },
+ onDragStopped = { velocity ->
+ onDragStopped(
+ layoutImpl,
+ transition,
+ velocity,
+ velocityThreshold,
+ positionalThreshold,
+ )
+ },
+ )
+}
+
+private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
+ var _currentScene by mutableStateOf(initialScene)
+ override val currentScene: SceneKey
+ get() = _currentScene.key
+
+ var _fromScene by mutableStateOf(initialScene)
+ override val fromScene: SceneKey
+ get() = _fromScene.key
+
+ var _toScene by mutableStateOf(initialScene)
+ override val toScene: SceneKey
+ get() = _toScene.key
+
+ override val progress: Float
+ get() {
+ val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+ if (distance == 0f) {
+ // This can happen only if fromScene == toScene.
+ error(
+ "Transition.progress should be called only when Transition.fromScene != " +
+ "Transition.toScene"
+ )
+ }
+ return offset / distance
+ }
+
+ /** The current offset caused by the drag gesture. */
+ var dragOffset by mutableFloatStateOf(0f)
+
+ /**
+ * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
+ */
+ var isAnimatingOffset by mutableStateOf(false)
+
+ /** The animatable used to animate the offset once the user lifted its finger. */
+ val offsetAnimatable = Animatable(0f, visibilityThreshold = OffsetVisibilityThreshold)
+
+ /**
+ * The job currently animating [offsetAnimatable], if it is animating. Note that setting this to
+ * a new job will automatically cancel the previous one.
+ */
+ var offsetAnimationJob: Job? = null
+ set(value) {
+ field?.cancel()
+ field = value
+ }
+
+ /** The absolute distance between [fromScene] and [toScene]. */
+ var absoluteDistance = 0f
+
+ /**
+ * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
+ * or to the left of [toScene].
+ */
+ var _distance by mutableFloatStateOf(0f)
+ val distance: Float
+ get() = _distance
+}
+
+/** The destination scene when swiping up or left from [this@upOrLeft]. */
+private fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
+ return when (orientation) {
+ Orientation.Vertical -> userActions[Swipe.Up]
+ Orientation.Horizontal -> userActions[Swipe.Left]
+ }
+}
+
+/** The destination scene when swiping down or right from [this@downOrRight]. */
+private fun Scene.downOrRight(orientation: Orientation): SceneKey? {
+ return when (orientation) {
+ Orientation.Vertical -> userActions[Swipe.Down]
+ Orientation.Horizontal -> userActions[Swipe.Right]
+ }
+}
+
+/** Whether swipe should be enabled in the given [orientation]. */
+private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
+ return upOrLeft(orientation) != null || downOrRight(orientation) != null
+}
+
+private fun Orientation.opposite(): Orientation {
+ return when (this) {
+ Orientation.Vertical -> Orientation.Horizontal
+ Orientation.Horizontal -> Orientation.Vertical
+ }
+}
+
+private fun onDragStarted(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: SwipeTransition,
+ orientation: Orientation,
+) {
+ if (layoutImpl.state.transitionState == transition) {
+ // This [transition] was already driving the animation: simply take over it.
+ if (transition.isAnimatingOffset) {
+ // Stop animating and start from where the current offset. Setting the animation job to
+ // `null` will effectively cancel the animation.
+ transition.isAnimatingOffset = false
+ transition.offsetAnimationJob = null
+ transition.dragOffset = transition.offsetAnimatable.value
+ }
+
+ return
+ }
+
+ // TODO(b/290184746): Better handle interruptions here if state != idle.
+
+ val fromScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
+
+ transition._currentScene = fromScene
+ transition._fromScene = fromScene
+
+ // We don't know where we are transitioning to yet given that the drag just started, so set it
+ // to fromScene, which will effectively be treated the same as Idle(fromScene).
+ transition._toScene = fromScene
+
+ transition.dragOffset = 0f
+ transition.isAnimatingOffset = false
+ transition.offsetAnimationJob = null
+
+ // Use the layout size in the swipe orientation for swipe distance.
+ // TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we
+ // will also have to make sure that we correctly handle overscroll.
+ transition.absoluteDistance =
+ when (orientation) {
+ Orientation.Horizontal -> layoutImpl.size.width
+ Orientation.Vertical -> layoutImpl.size.height
+ }.toFloat()
+
+ if (transition.absoluteDistance > 0f) {
+ layoutImpl.state.transitionState = transition
+ }
+}
+
+private fun onDrag(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: SwipeTransition,
+ orientation: Orientation,
+ delta: Float,
+) {
+ transition.dragOffset += delta
+
+ // First check transition.fromScene should be changed for the case where the user quickly swiped
+ // twice in a row to accelerate the transition and go from A => B then B => C really fast.
+ maybeHandleAcceleratedSwipe(transition, orientation)
+
+ val fromScene = transition._fromScene
+ val upOrLeft = fromScene.upOrLeft(orientation)
+ val downOrRight = fromScene.downOrRight(orientation)
+ val offset = transition.dragOffset
+
+ // Compute the target scene depending on the current offset.
+ val targetSceneKey: SceneKey
+ val signedDistance: Float
+ when {
+ offset < 0f && upOrLeft != null -> {
+ targetSceneKey = upOrLeft
+ signedDistance = -transition.absoluteDistance
+ }
+ offset > 0f && downOrRight != null -> {
+ targetSceneKey = downOrRight
+ signedDistance = transition.absoluteDistance
+ }
+ else -> {
+ targetSceneKey = fromScene.key
+ signedDistance = 0f
+ }
+ }
+
+ if (transition._toScene.key != targetSceneKey) {
+ transition._toScene = layoutImpl.scenes.getValue(targetSceneKey)
+ }
+
+ if (transition._distance != signedDistance) {
+ transition._distance = signedDistance
+ }
+}
+
+/**
+ * Change fromScene in the case where the user quickly swiped multiple times in the same direction
+ * to accelerate the transition from A => B then B => C.
+ */
+private fun maybeHandleAcceleratedSwipe(
+ transition: SwipeTransition,
+ orientation: Orientation,
+) {
+ val toScene = transition._toScene
+ val fromScene = transition._fromScene
+
+ // If the swipe was not committed, don't do anything.
+ if (fromScene == toScene || transition._currentScene != toScene) {
+ return
+ }
+
+ // If the offset is past the distance then let's change fromScene so that the user can swipe to
+ // the next screen or go back to the previous one.
+ val offset = transition.dragOffset
+ val absoluteDistance = transition.absoluteDistance
+ if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
+ transition.dragOffset += absoluteDistance
+ transition._fromScene = toScene
+ } else if (offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key) {
+ transition.dragOffset -= absoluteDistance
+ transition._fromScene = toScene
+ }
+
+ // Important note: toScene and distance will be updated right after this function is called,
+ // using fromScene and dragOffset.
+}
+
+private fun CoroutineScope.onDragStopped(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: SwipeTransition,
+ velocity: Float,
+ velocityThreshold: Float,
+ positionalThreshold: Float,
+) {
+ // The state was changed since the drag started; don't do anything.
+ if (layoutImpl.state.transitionState != transition) {
+ return
+ }
+
+ // We were not animating.
+ if (transition._fromScene == transition._toScene) {
+ layoutImpl.state.transitionState = TransitionState.Idle(transition._fromScene.key)
+ return
+ }
+
+ // Compute the destination scene (and therefore offset) to settle in.
+ val targetScene: Scene
+ val targetOffset: Float
+ val offset = transition.dragOffset
+ val distance = transition.distance
+ if (
+ shouldCommitSwipe(
+ offset,
+ distance,
+ velocity,
+ velocityThreshold,
+ positionalThreshold,
+ wasCommitted = transition._currentScene == transition._toScene,
+ )
+ ) {
+ targetOffset = distance
+ targetScene = transition._toScene
+ } else {
+ targetOffset = 0f
+ targetScene = transition._fromScene
+ }
+
+ // If the effective current scene changed, it should be reflected right now in the current scene
+ // state, even before the settle animation is ongoing. That way all the swipeables and back
+ // handlers will be refreshed and the user can for instance quickly swipe vertically from A => B
+ // then horizontally from B => C, or swipe from A => B then immediately go back B => A.
+ if (targetScene != transition._currentScene) {
+ transition._currentScene = targetScene
+ layoutImpl.onChangeScene(targetScene.key)
+ }
+
+ // Animate the offset.
+ transition.offsetAnimationJob = launch {
+ transition.offsetAnimatable.snapTo(offset)
+ transition.isAnimatingOffset = true
+
+ transition.offsetAnimatable.animateTo(
+ targetOffset,
+ // TODO(b/290184746): Make this spring spec configurable.
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = OffsetVisibilityThreshold
+ ),
+ initialVelocity = velocity,
+ )
+
+ // Now that the animation is done, the state should be idle. Note that if the state was
+ // changed since this animation started, some external code changed it and we shouldn't do
+ // anything here. Note also that this job will be cancelled in the case where the user
+ // intercepts this swipe.
+ if (layoutImpl.state.transitionState == transition) {
+ layoutImpl.state.transitionState = TransitionState.Idle(targetScene.key)
+ }
+
+ transition.offsetAnimationJob = null
+ }
+}
+
+/**
+ * Whether the swipe to the target scene should be committed or not. This is inspired by
+ * SwipeableV2.computeTarget().
+ */
+private fun shouldCommitSwipe(
+ offset: Float,
+ distance: Float,
+ velocity: Float,
+ velocityThreshold: Float,
+ positionalThreshold: Float,
+ wasCommitted: Boolean,
+): Boolean {
+ fun isCloserToTarget(): Boolean {
+ return (offset - distance).absoluteValue < offset.absoluteValue
+ }
+
+ // Swiping up or left.
+ if (distance < 0f) {
+ return if (offset > 0f || velocity >= velocityThreshold) {
+ false
+ } else {
+ velocity <= -velocityThreshold ||
+ (offset <= -positionalThreshold && !wasCommitted) ||
+ isCloserToTarget()
+ }
+ }
+
+ // Swiping down or right.
+ return if (offset < 0f || velocity <= -velocityThreshold) {
+ false
+ } else {
+ velocity >= velocityThreshold ||
+ (offset >= positionalThreshold && !wasCommitted) ||
+ isCloserToTarget()
+ }
+}
+
+/**
+ * The number of pixels below which there won't be a visible difference in the transition and from
+ * which the animation can stop.
+ */
+private const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
new file mode 100644
index 000000000000..fb12b90d7d3e
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
+fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
+ return transitionsImpl(builder)
+}
+
+@DslMarker annotation class TransitionDsl
+
+@TransitionDsl
+interface SceneTransitionsBuilder {
+ /**
+ * Define the default animation to be played when transitioning [to] the specified scene, from
+ * any scene. For the animation specification to apply only when transitioning between two
+ * specific scenes, use [from] instead.
+ *
+ * @see from
+ */
+ fun to(
+ to: SceneKey,
+ builder: TransitionBuilder.() -> Unit = {},
+ ): TransitionSpec
+
+ /**
+ * Define the animation to be played when transitioning [from] the specified scene. For the
+ * animation specification to apply only when transitioning between two specific scenes, pass
+ * the destination scene via the [to] argument.
+ *
+ * When looking up which transition should be used when animating from scene A to scene B, we
+ * pick the single transition matching one of these predicates (in order of importance):
+ * 1. from == A && to == B
+ * 2. to == A && from == B, which is then treated in reverse.
+ * 3. (from == A && to == null) || (from == null && to == B)
+ * 4. (from == B && to == null) || (from == null && to == A), which is then treated in reverse.
+ */
+ fun from(
+ from: SceneKey,
+ to: SceneKey? = null,
+ builder: TransitionBuilder.() -> Unit = {},
+ ): TransitionSpec
+}
+
+@TransitionDsl
+interface TransitionBuilder : PropertyTransformationBuilder {
+ /**
+ * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
+ * performing programmatic (not input pointer tracking) animations.
+ */
+ var spec: AnimationSpec<Float>
+
+ /**
+ * Define a progress-based range for the transformations inside [builder].
+ *
+ * For instance, the following will fade `Foo` during the first half of the transition then it
+ * will translate it by 100.dp during the second half.
+ *
+ * ```
+ * fractionRange(end = 0.5f) { fade(Foo) }
+ * fractionRange(start = 0.5f) { translate(Foo, x = 100.dp) }
+ * ```
+ *
+ * @param start the start of the range, in the [0; 1] range.
+ * @param end the end of the range, in the [0; 1] range.
+ */
+ fun fractionRange(
+ start: Float? = null,
+ end: Float? = null,
+ builder: PropertyTransformationBuilder.() -> Unit,
+ )
+
+ /**
+ * Define a timestamp-based range for the transformations inside [builder].
+ *
+ * For instance, the following will fade `Foo` during the first half of the transition then it
+ * will translate it by 100.dp during the second half.
+ *
+ * ```
+ * spec = tween(500)
+ * timestampRange(end = 250) { fade(Foo) }
+ * timestampRange(start = 250) { translate(Foo, x = 100.dp) }
+ * ```
+ *
+ * Important: [spec] must be a [androidx.compose.animation.core.DurationBasedAnimationSpec] if
+ * you call [timestampRange], otherwise this will throw. The spec duration will be used to
+ * transform this range into a [fractionRange].
+ *
+ * @param startMillis the start of the range, in the [0; spec.duration] range.
+ * @param endMillis the end of the range, in the [0; spec.duration] range.
+ */
+ fun timestampRange(
+ startMillis: Int? = null,
+ endMillis: Int? = null,
+ builder: PropertyTransformationBuilder.() -> Unit,
+ )
+
+ /**
+ * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and
+ * using the given [shape].
+ *
+ * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
+ * This can be used to make content drawn below an opaque element visible. For example, if we
+ * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
+ * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
+ * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
+ * the result.
+ */
+ fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape)
+}
+
+@TransitionDsl
+interface PropertyTransformationBuilder {
+ /**
+ * Fade the element(s) matching [matcher]. This will automatically fade in or fade out if the
+ * element is entering or leaving the scene, respectively.
+ */
+ fun fade(matcher: ElementMatcher)
+
+ /** Translate the element(s) matching [matcher] by ([x], [y]) dp. */
+ fun translate(matcher: ElementMatcher, x: Dp = 0.dp, y: Dp = 0.dp)
+
+ /**
+ * Translate the element(s) matching [matcher] from/to the [edge] of the [SceneTransitionLayout]
+ * animating it.
+ *
+ * If [startsOutsideLayoutBounds] is `true`, then the element will start completely outside of
+ * the layout bounds (i.e. none of it will be visible at progress = 0f if the layout clips its
+ * content). If it is `false`, then the element will start aligned with the edge of the layout
+ * (i.e. it will be completely visible at progress = 0f).
+ */
+ fun translate(matcher: ElementMatcher, edge: Edge, startsOutsideLayoutBounds: Boolean = true)
+
+ /**
+ * Translate the element(s) matching [matcher] by the same amount that [anchor] is translated
+ * during this transition.
+ *
+ * Note: This currently only works if [anchor] is a shared element of this transition.
+ *
+ * TODO(b/290184746): Also support anchors that are not shared but translated because of other
+ * transformations, like an edge translation.
+ */
+ fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey)
+
+ /**
+ * Scale the [width] and [height] of the element(s) matching [matcher]. Note that this scaling
+ * is done during layout, so it will potentially impact the size and position of other elements.
+ *
+ * TODO(b/290184746): Also provide a scaleDrawing() to scale an element at drawing time.
+ */
+ fun scaleSize(matcher: ElementMatcher, width: Float = 1f, height: Float = 1f)
+
+ /**
+ * Scale the element(s) matching [matcher] so that it grows/shrinks to the same size as [anchor]
+ * .
+ *
+ * Note: This currently only works if [anchor] is a shared element of this transition.
+ */
+ fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey)
+}
+
+/** An interface to match one or more elements. */
+interface ElementMatcher {
+ /** Whether the element with key [key] matches this matcher. */
+ fun matches(key: ElementKey): Boolean
+}
+
+/** The edge of a [SceneTransitionLayout]. */
+enum class Edge {
+ Left,
+ Right,
+ Top,
+ Bottom,
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
new file mode 100644
index 000000000000..afd49b4fde09
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DurationBasedAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import com.android.compose.animation.scene.transformation.AnchoredSize
+import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.EdgeTranslate
+import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.PunchHole
+import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
+import com.android.compose.animation.scene.transformation.ScaleSize
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.TransformationRange
+import com.android.compose.animation.scene.transformation.Translate
+
+internal fun transitionsImpl(
+ builder: SceneTransitionsBuilder.() -> Unit,
+): SceneTransitions {
+ val impl = SceneTransitionsBuilderImpl().apply(builder)
+ return SceneTransitions(impl.transitionSpecs)
+}
+
+private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
+ val transitionSpecs = mutableListOf<TransitionSpec>()
+
+ override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
+ return transition(from = null, to = to, builder)
+ }
+
+ override fun from(
+ from: SceneKey,
+ to: SceneKey?,
+ builder: TransitionBuilder.() -> Unit
+ ): TransitionSpec {
+ return transition(from = from, to = to, builder)
+ }
+
+ private fun transition(
+ from: SceneKey?,
+ to: SceneKey?,
+ builder: TransitionBuilder.() -> Unit,
+ ): TransitionSpec {
+ val impl = TransitionBuilderImpl().apply(builder)
+ val spec =
+ TransitionSpec(
+ from,
+ to,
+ impl.transformations,
+ impl.spec,
+ )
+ transitionSpecs.add(spec)
+ return spec
+ }
+}
+
+private class TransitionBuilderImpl : TransitionBuilder {
+ val transformations = mutableListOf<Transformation>()
+ override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
+
+ private var range: TransformationRange? = null
+ private val durationMillis: Int by lazy {
+ val spec = spec
+ if (spec !is DurationBasedAnimationSpec) {
+ error("timestampRange {} can only be used with a DurationBasedAnimationSpec")
+ }
+
+ spec.vectorize(Float.VectorConverter).durationMillis
+ }
+
+ override fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape) {
+ transformations.add(PunchHole(matcher, bounds, shape))
+ }
+
+ override fun fractionRange(
+ start: Float?,
+ end: Float?,
+ builder: PropertyTransformationBuilder.() -> Unit
+ ) {
+ range = TransformationRange(start, end)
+ builder()
+ range = null
+ }
+
+ override fun timestampRange(
+ startMillis: Int?,
+ endMillis: Int?,
+ builder: PropertyTransformationBuilder.() -> Unit
+ ) {
+ if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) {
+ error("invalid start value: startMillis=$startMillis durationMillis=$durationMillis")
+ }
+
+ if (endMillis != null && (endMillis < 0 || endMillis > durationMillis)) {
+ error("invalid end value: endMillis=$startMillis durationMillis=$durationMillis")
+ }
+
+ val start = startMillis?.let { it.toFloat() / durationMillis }
+ val end = endMillis?.let { it.toFloat() / durationMillis }
+ fractionRange(start, end, builder)
+ }
+
+ private fun transformation(transformation: PropertyTransformation<*>) {
+ if (range != null) {
+ transformations.add(RangedPropertyTransformation(transformation, range!!))
+ } else {
+ transformations.add(transformation)
+ }
+ }
+
+ override fun fade(matcher: ElementMatcher) {
+ transformation(Fade(matcher))
+ }
+
+ override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) {
+ transformation(Translate(matcher, x, y))
+ }
+
+ override fun translate(
+ matcher: ElementMatcher,
+ edge: Edge,
+ startsOutsideLayoutBounds: Boolean
+ ) {
+ transformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
+ }
+
+ override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) {
+ transformation(AnchoredTranslate(matcher, anchor))
+ }
+
+ override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) {
+ transformation(ScaleSize(matcher, width, height))
+ }
+
+ override fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey) {
+ transformation(AnchoredSize(matcher, anchor))
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
new file mode 100644
index 000000000000..d4ed697f1757
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Anchor the size of an element to the size of another element. */
+internal class AnchoredSize(
+ override val matcher: ElementMatcher,
+ private val anchor: ElementKey,
+) : PropertyTransformation<IntSize> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: IntSize,
+ ): IntSize {
+ fun anchorSizeIn(scene: SceneKey): IntSize {
+ val size = layoutImpl.elements[anchor]?.sceneValues?.get(scene)?.size
+ return if (size != null && size != Element.SizeUnspecified) {
+ size
+ } else {
+ value
+ }
+ }
+
+ // This simple implementation assumes that the size of [element] is the same as the size of
+ // the [anchor] in [scene], so simply transform to the size of the anchor in the other
+ // scene.
+ return if (scene.key == transition.fromScene) {
+ anchorSizeIn(transition.toScene)
+ } else {
+ anchorSizeIn(transition.fromScene)
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
new file mode 100644
index 000000000000..8a5bd746dced
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Anchor the translation of an element to another element. */
+internal class AnchoredTranslate(
+ override val matcher: ElementMatcher,
+ private val anchor: ElementKey,
+) : PropertyTransformation<Offset> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Offset,
+ ): Offset {
+ val anchor = layoutImpl.elements[anchor] ?: return value
+ fun anchorOffsetIn(scene: SceneKey): Offset? {
+ return anchor.sceneValues[scene]?.offset?.takeIf { it.isSpecified }
+ }
+
+ // [element] will move the same amount as [anchor] does.
+ // TODO(b/290184746): Also support anchors that are not shared but translated because of
+ // other transformations, like an edge translation.
+ val anchorFromOffset = anchorOffsetIn(transition.fromScene) ?: return value
+ val anchorToOffset = anchorOffsetIn(transition.toScene) ?: return value
+ val offset = anchorToOffset - anchorFromOffset
+
+ return if (scene.key == transition.toScene) {
+ Offset(
+ value.x - offset.x,
+ value.y - offset.y,
+ )
+ } else {
+ Offset(
+ value.x + offset.x,
+ value.y + offset.y,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
new file mode 100644
index 000000000000..5cdce9489772
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Translate an element from an edge of the layout. */
+internal class EdgeTranslate(
+ override val matcher: ElementMatcher,
+ private val edge: Edge,
+ private val startsOutsideLayoutBounds: Boolean = true,
+) : PropertyTransformation<Offset> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Offset
+ ): Offset {
+ val sceneSize = scene.size
+ val elementSize = sceneValues.size
+ if (elementSize == Element.SizeUnspecified) {
+ return value
+ }
+
+ return when (edge) {
+ Edge.Top ->
+ if (startsOutsideLayoutBounds) {
+ Offset(value.x, -elementSize.height.toFloat())
+ } else {
+ Offset(value.x, 0f)
+ }
+ Edge.Left ->
+ if (startsOutsideLayoutBounds) {
+ Offset(-elementSize.width.toFloat(), value.y)
+ } else {
+ Offset(0f, value.y)
+ }
+ Edge.Bottom ->
+ if (startsOutsideLayoutBounds) {
+ Offset(value.x, sceneSize.height.toFloat())
+ } else {
+ Offset(value.x, (sceneSize.height - elementSize.height).toFloat())
+ }
+ Edge.Right ->
+ if (startsOutsideLayoutBounds) {
+ Offset(sceneSize.width.toFloat(), value.y)
+ } else {
+ Offset((sceneSize.width - elementSize.width).toFloat(), value.y)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt
new file mode 100644
index 000000000000..0a5ac5413b38
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Fade an element in or out. */
+internal class Fade(
+ override val matcher: ElementMatcher,
+) : PropertyTransformation<Float> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Float
+ ): Float {
+ // Return the alpha value of [element] either when it starts fading in or when it finished
+ // fading out, which is `0` in both cases.
+ return 0f
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt
new file mode 100644
index 000000000000..31e7d7c7c6ba
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.withSaveLayer
+import androidx.compose.ui.unit.toSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+
+/** Punch a hole in an element using the bounds of another element and a given [shape]. */
+internal class PunchHole(
+ override val matcher: ElementMatcher,
+ private val bounds: ElementKey,
+ private val shape: Shape,
+) : ModifierTransformation {
+ override fun Modifier.transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ ): Modifier {
+ return drawWithContent {
+ val bounds = layoutImpl.elements[bounds]
+ if (
+ bounds == null ||
+ bounds.lastSize == Element.SizeUnspecified ||
+ bounds.lastOffset == Offset.Unspecified
+ ) {
+ drawContent()
+ return@drawWithContent
+ }
+
+ drawIntoCanvas { canvas ->
+ canvas.withSaveLayer(size.toRect(), Paint()) {
+ drawContent()
+
+ val offset = bounds.lastOffset - element.lastOffset
+ translate(offset.x, offset.y) { drawHole(bounds) }
+ }
+ }
+ }
+ }
+
+ private fun DrawScope.drawHole(bounds: Element) {
+ if (shape == RectangleShape) {
+ drawRect(Color.Black, blendMode = BlendMode.DstOut)
+ return
+ }
+
+ // TODO(b/290184746): Cache outline if the size of bounds does not change.
+ drawOutline(
+ shape.createOutline(
+ bounds.lastSize.toSize(),
+ layoutDirection,
+ this,
+ ),
+ Color.Black,
+ blendMode = BlendMode.DstOut,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
new file mode 100644
index 000000000000..ce754dc76adc
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+import kotlin.math.roundToInt
+
+/**
+ * Scales the size of an element. Note that this makes the element resize every frame and will
+ * therefore impact the layout of other elements.
+ */
+internal class ScaleSize(
+ override val matcher: ElementMatcher,
+ private val width: Float = 1f,
+ private val height: Float = 1f,
+) : PropertyTransformation<IntSize> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: IntSize,
+ ): IntSize {
+ return IntSize(
+ width = (value.width * width).roundToInt(),
+ height = (value.height * height).roundToInt(),
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
new file mode 100644
index 000000000000..ce6749da2711
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** A transformation applied to one or more elements during a transition. */
+sealed interface Transformation {
+ /**
+ * The matcher that should match the element(s) to which this transformation should be applied.
+ */
+ val matcher: ElementMatcher
+
+ /*
+ * Reverse this transformation. This is called when we use Transition(from = A, to = B) when
+ * animating from B to A and there is no Transition(from = B, to = A) defined.
+ */
+ fun reverse(): Transformation = this
+}
+
+/** A transformation that is applied on the element during the whole transition. */
+internal interface ModifierTransformation : Transformation {
+ /** Apply the transformation to [element]. */
+ // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
+ // to these internal classes.
+ fun Modifier.transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ ): Modifier
+}
+
+/** A transformation that changes the value of an element property, like its size or offset. */
+internal sealed interface PropertyTransformation<T> : Transformation {
+ /**
+ * The range during which the transformation is applied. If it is `null`, then the
+ * transformation will be applied throughout the whole scene transition.
+ */
+ val range: TransformationRange?
+ get() = null
+
+ /**
+ * Transform [value], i.e. the value of the transformed property without this transformation.
+ */
+ // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
+ // to these internal classes.
+ fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: T,
+ ): T
+}
+
+/**
+ * A [PropertyTransformation] associated to a range. This is a helper class so that normal
+ * implementations of [PropertyTransformation] don't have to take care of reversing their range when
+ * they are reversed.
+ */
+internal class RangedPropertyTransformation<T>(
+ val delegate: PropertyTransformation<T>,
+ override val range: TransformationRange,
+) : PropertyTransformation<T> by delegate {
+ override fun reverse(): Transformation {
+ return RangedPropertyTransformation(
+ delegate.reverse() as PropertyTransformation<T>,
+ range.reverse()
+ )
+ }
+}
+
+/** The progress-based range of a [PropertyTransformation]. */
+data class TransformationRange
+private constructor(
+ val start: Float,
+ val end: Float,
+) {
+ constructor(
+ start: Float? = null,
+ end: Float? = null
+ ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified)
+
+ init {
+ require(!start.isSpecified() || (start in 0f..1f))
+ require(!end.isSpecified() || (end in 0f..1f))
+ require(!start.isSpecified() || !end.isSpecified() || start <= end)
+ }
+
+ /** Reverse this range. */
+ fun reverse() = TransformationRange(start = reverseBound(end), end = reverseBound(start))
+
+ /** Get the progress of this range given the global [transitionProgress]. */
+ fun progress(transitionProgress: Float): Float {
+ return when {
+ start.isSpecified() && end.isSpecified() ->
+ ((transitionProgress - start) / (end - start)).coerceIn(0f, 1f)
+ !start.isSpecified() && !end.isSpecified() -> transitionProgress
+ end.isSpecified() -> (transitionProgress / end).coerceAtMost(1f)
+ else -> ((transitionProgress - start) / (1f - start)).coerceAtLeast(0f)
+ }
+ }
+
+ private fun Float.isSpecified() = this != BoundUnspecified
+
+ private fun reverseBound(bound: Float): Float {
+ return if (bound.isSpecified()) {
+ 1f - bound
+ } else {
+ BoundUnspecified
+ }
+ }
+
+ companion object {
+ private const val BoundUnspecified = Float.MIN_VALUE
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt
new file mode 100644
index 000000000000..8abca61bab20
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Translate an element by a fixed amount of density-independent pixels. */
+internal class Translate(
+ override val matcher: ElementMatcher,
+ private val x: Dp = 0.dp,
+ private val y: Dp = 0.dp,
+) : PropertyTransformation<Offset> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Offset,
+ ): Offset {
+ return with(layoutImpl.density) {
+ Offset(
+ value.x + x.toPx(),
+ value.y + y.toPx(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
index 5224c51bb7c3..27f0948d5377 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
@@ -22,7 +22,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import kotlin.math.ceil
import kotlin.math.max
@@ -126,18 +125,20 @@ private fun Grid(
((columns - 1) * horizontalSpacing.toPx()).roundToInt()
val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt()
val childConstraints =
- Constraints().apply {
- if (constraints.maxWidth != Constraints.Infinity) {
- constrainWidth(
+ Constraints(
+ maxWidth =
+ if (constraints.maxWidth != Constraints.Infinity) {
(constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns
- )
- }
- if (constraints.maxHeight != Constraints.Infinity) {
- constrainWidth(
+ } else {
+ Constraints.Infinity
+ },
+ maxHeight =
+ if (constraints.maxHeight != Constraints.Infinity) {
(constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows
- )
- }
- }
+ } else {
+ Constraints.Infinity
+ }
+ )
val placeables = buildList {
for (cellIndex in measurables.indices) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
index 83071d78c64d..135a6e4ec4e4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.compose.modifiers
+package com.android.compose.modifiers
import androidx.compose.ui.Modifier
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt
deleted file mode 100644
index a80a1f934dab..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.flow.filter
-
-/** Library-wide switch to turn on debug logging. */
-internal const val DebugLog = false
-
-@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
-@Retention(AnnotationRetention.BINARY)
-annotation class ExperimentalPagerApi
-
-/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */
-@ExperimentalPagerApi
-object PagerDefaults {
- /**
- * Remember the default [FlingBehavior] that represents the scroll curve.
- *
- * @param state The [PagerState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- */
- @Composable
- fun flingBehavior(
- state: PagerState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
- ): FlingBehavior =
- rememberSnappingFlingBehavior(
- lazyListState = state.lazyListState,
- decayAnimationSpec = decayAnimationSpec,
- snapAnimationSpec = snapAnimationSpec,
- )
-
- @Deprecated(
- "Replaced with PagerDefaults.flingBehavior()",
- ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)")
- )
- @Composable
- fun rememberPagerFlingConfig(
- state: PagerState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
- ): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec)
-}
-
-/**
- * A horizontally scrolling layout that allows users to flip between items to the left and right.
- *
- * @param count the number of pages.
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the pager's state.
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item
- * is located at the end.
- * @param itemSpacing horizontal spacing to add between items.
- * @param flingBehavior logic describing fling behavior.
- * @param key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param content a block which describes the content. Inside this block you can reference
- * [PagerScope.currentPage] and other properties in [PagerScope].
- * @sample com.google.accompanist.sample.pager.HorizontalPagerSample
- */
-@ExperimentalPagerApi
-@Composable
-fun HorizontalPager(
- count: Int,
- modifier: Modifier = Modifier,
- state: PagerState = rememberPagerState(),
- reverseLayout: Boolean = false,
- itemSpacing: Dp = 0.dp,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- key: ((page: Int) -> Any)? = null,
- contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- Pager(
- count = count,
- state = state,
- modifier = modifier,
- isVertical = false,
- reverseLayout = reverseLayout,
- itemSpacing = itemSpacing,
- verticalAlignment = verticalAlignment,
- flingBehavior = flingBehavior,
- key = key,
- contentPadding = contentPadding,
- content = content
- )
-}
-
-/**
- * A vertically scrolling layout that allows users to flip between items to the top and bottom.
- *
- * @param count the number of pages.
- * @param modifier the modifier to apply to this layout.
- * @param state the state object to be used to control or observe the pager's state.
- * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
- * composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item
- * is located at the bottom.
- * @param itemSpacing vertical spacing to add between items.
- * @param flingBehavior logic describing fling behavior.
- * @param key the scroll position will be maintained based on the key, which means if you add/remove
- * items before the current visible item the item with the given key will be kept as the first
- * visible one.
- * @param content a block which describes the content. Inside this block you can reference
- * [PagerScope.currentPage] and other properties in [PagerScope].
- * @sample com.google.accompanist.sample.pager.VerticalPagerSample
- */
-@ExperimentalPagerApi
-@Composable
-fun VerticalPager(
- count: Int,
- modifier: Modifier = Modifier,
- state: PagerState = rememberPagerState(),
- reverseLayout: Boolean = false,
- itemSpacing: Dp = 0.dp,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
- horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- key: ((page: Int) -> Any)? = null,
- contentPadding: PaddingValues = PaddingValues(0.dp),
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- Pager(
- count = count,
- state = state,
- modifier = modifier,
- isVertical = true,
- reverseLayout = reverseLayout,
- itemSpacing = itemSpacing,
- horizontalAlignment = horizontalAlignment,
- flingBehavior = flingBehavior,
- key = key,
- contentPadding = contentPadding,
- content = content
- )
-}
-
-@ExperimentalPagerApi
-@Composable
-internal fun Pager(
- count: Int,
- modifier: Modifier,
- state: PagerState,
- reverseLayout: Boolean,
- itemSpacing: Dp,
- isVertical: Boolean,
- flingBehavior: FlingBehavior,
- key: ((page: Int) -> Any)?,
- contentPadding: PaddingValues,
- verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- content: @Composable PagerScope.(page: Int) -> Unit,
-) {
- require(count >= 0) { "pageCount must be >= 0" }
-
- // Provide our PagerState with access to the SnappingFlingBehavior animation target
- // TODO: can this be done in a better way?
- state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget }
-
- LaunchedEffect(count) {
- state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
- }
-
- // Once a fling (scroll) has finished, notify the state
- LaunchedEffect(state) {
- // When a 'scroll' has finished, notify the state
- snapshotFlow { state.isScrollInProgress }
- .filter { !it }
- .collect { state.onScrollFinished() }
- }
-
- val pagerScope = remember(state) { PagerScopeImpl(state) }
-
- // We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
- // as normal
- val consumeFlingNestedScrollConnection =
- ConsumeFlingNestedScrollConnection(
- consumeHorizontal = !isVertical,
- consumeVertical = isVertical,
- )
-
- if (isVertical) {
- LazyColumn(
- state = state.lazyListState,
- verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
- horizontalAlignment = horizontalAlignment,
- flingBehavior = flingBehavior,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding,
- modifier = modifier,
- ) {
- items(
- count = count,
- key = key,
- ) { page ->
- Box(
- Modifier
- // We don't any nested flings to continue in the pager, so we add a
- // connection which consumes them.
- // See: https://github.com/google/accompanist/issues/347
- .nestedScroll(connection = consumeFlingNestedScrollConnection)
- // Constraint the content to be <= than the size of the pager.
- .fillParentMaxHeight()
- .wrapContentSize()
- ) {
- pagerScope.content(page)
- }
- }
- }
- } else {
- LazyRow(
- state = state.lazyListState,
- verticalAlignment = verticalAlignment,
- horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
- flingBehavior = flingBehavior,
- reverseLayout = reverseLayout,
- contentPadding = contentPadding,
- modifier = modifier,
- ) {
- items(
- count = count,
- key = key,
- ) { page ->
- Box(
- Modifier
- // We don't any nested flings to continue in the pager, so we add a
- // connection which consumes them.
- // See: https://github.com/google/accompanist/issues/347
- .nestedScroll(connection = consumeFlingNestedScrollConnection)
- // Constraint the content to be <= than the size of the pager.
- .fillParentMaxWidth()
- .wrapContentSize()
- ) {
- pagerScope.content(page)
- }
- }
- }
- }
-}
-
-private class ConsumeFlingNestedScrollConnection(
- private val consumeHorizontal: Boolean,
- private val consumeVertical: Boolean,
-) : NestedScrollConnection {
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset =
- when (source) {
- // We can consume all resting fling scrolls so that they don't propagate up to the
- // Pager
- NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
- else -> Offset.Zero
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- // We can consume all post fling velocity on the main-axis
- // so that it doesn't propagate up to the Pager
- return available.consume(consumeHorizontal, consumeVertical)
- }
-}
-
-private fun Offset.consume(
- consumeHorizontal: Boolean,
- consumeVertical: Boolean,
-): Offset =
- Offset(
- x = if (consumeHorizontal) this.x else 0f,
- y = if (consumeVertical) this.y else 0f,
- )
-
-private fun Velocity.consume(
- consumeHorizontal: Boolean,
- consumeVertical: Boolean,
-): Velocity =
- Velocity(
- x = if (consumeHorizontal) this.x else 0f,
- y = if (consumeVertical) this.y else 0f,
- )
-
-/** Scope for [HorizontalPager] content. */
-@ExperimentalPagerApi
-@Stable
-interface PagerScope {
- /** Returns the current selected page */
- val currentPage: Int
-
- /** The current offset from the start of [currentPage], as a ratio of the page width. */
- val currentPageOffset: Float
-}
-
-@ExperimentalPagerApi
-private class PagerScopeImpl(
- private val state: PagerState,
-) : PagerScope {
- override val currentPage: Int
- get() = state.currentPage
- override val currentPageOffset: Float
- get() = state.currentPageOffset
-}
-
-/**
- * Calculate the offset for the given [page] from the current scroll position. This is useful when
- * using the scroll position to apply effects or animations to items.
- *
- * The returned offset can positive or negative, depending on whether which direction the [page] is
- * compared to the current scroll position.
- *
- * @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
- */
-@ExperimentalPagerApi
-fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
- return (currentPage + currentPageOffset) - page
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt
deleted file mode 100644
index 1822a68f1e77..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.pager
-
-import androidx.annotation.FloatRange
-import androidx.annotation.IntRange
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.spring
-import androidx.compose.foundation.MutatePriority
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.gestures.ScrollableState
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.listSaver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import kotlin.math.absoluteValue
-import kotlin.math.roundToInt
-
-@Deprecated(
- "Replaced with rememberPagerState(initialPage) and count parameter on Pager composables",
- ReplaceWith("rememberPagerState(initialPage)"),
- level = DeprecationLevel.ERROR,
-)
-@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
-@ExperimentalPagerApi
-@Composable
-inline fun rememberPagerState(
- @IntRange(from = 0) pageCount: Int,
- @IntRange(from = 0) initialPage: Int = 0,
- @FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f,
- @IntRange(from = 1) initialOffscreenLimit: Int = 1,
- infiniteLoop: Boolean = false
-): PagerState {
- return rememberPagerState(initialPage = initialPage)
-}
-
-/**
- * Creates a [PagerState] that is remembered across compositions.
- *
- * Changes to the provided values for [initialPage] will **not** result in the state being recreated
- * or changed in any way if it has already been created.
- *
- * @param initialPage the initial value for [PagerState.currentPage]
- */
-@ExperimentalPagerApi
-@Composable
-fun rememberPagerState(
- @IntRange(from = 0) initialPage: Int = 0,
-): PagerState =
- rememberSaveable(saver = PagerState.Saver) {
- PagerState(
- currentPage = initialPage,
- )
- }
-
-/**
- * A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
- *
- * In most cases, this will be created via [rememberPagerState].
- *
- * @param currentPage the initial value for [PagerState.currentPage]
- */
-@ExperimentalPagerApi
-@Stable
-class PagerState(
- @IntRange(from = 0) currentPage: Int = 0,
-) : ScrollableState {
- // Should this be public?
- internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
-
- private var _currentPage by mutableStateOf(currentPage)
-
- private val currentLayoutPageInfo: LazyListItemInfo?
- get() =
- lazyListState.layoutInfo.visibleItemsInfo
- .asSequence()
- .filter { it.offset <= 0 && it.offset + it.size > 0 }
- .lastOrNull()
-
- private val currentLayoutPageOffset: Float
- get() =
- currentLayoutPageInfo?.let { current ->
- // We coerce since itemSpacing can make the offset > 1f.
- // We don't want to count spacing in the offset so cap it to 1f
- (-current.offset / current.size.toFloat()).coerceIn(0f, 1f)
- }
- ?: 0f
-
- /**
- * [InteractionSource] that will be used to dispatch drag events when this list is being
- * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
- * [isScrollInProgress].
- */
- val interactionSource: InteractionSource
- get() = lazyListState.interactionSource
-
- /** The number of pages to display. */
- @get:IntRange(from = 0)
- val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount }
-
- /**
- * The index of the currently selected page. This may not be the page which is currently
- * displayed on screen.
- *
- * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
- */
- @get:IntRange(from = 0)
- var currentPage: Int
- get() = _currentPage
- internal set(value) {
- if (value != _currentPage) {
- _currentPage = value
- }
- }
-
- /**
- * The current offset from the start of [currentPage], as a ratio of the page width.
- *
- * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
- */
- val currentPageOffset: Float by derivedStateOf {
- currentLayoutPageInfo?.let {
- // The current page offset is the current layout page delta from `currentPage`
- // (which is only updated after a scroll/animation).
- // We calculate this by looking at the current layout page + it's offset,
- // then subtracting the 'current page'.
- it.index + currentLayoutPageOffset - _currentPage
- }
- ?: 0f
- }
-
- /** The target page for any on-going animations. */
- private var animationTargetPage: Int? by mutableStateOf(null)
-
- internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null)
-
- /**
- * The target page for any on-going animations or scrolls by the user. Returns the current page
- * if a scroll or animation is not currently in progress.
- */
- val targetPage: Int
- get() =
- animationTargetPage
- ?: flingAnimationTarget?.invoke()
- ?: when {
- // If a scroll isn't in progress, return the current page
- !isScrollInProgress -> currentPage
- // If the offset is 0f (or very close), return the current page
- currentPageOffset.absoluteValue < 0.001f -> currentPage
- // If we're offset towards the start, guess the previous page
- currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0)
- // If we're offset towards the end, guess the next page
- else -> (currentPage + 1).coerceAtMost(pageCount - 1)
- }
-
- @Deprecated(
- "Replaced with animateScrollToPage(page, pageOffset)",
- ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)")
- )
- @Suppress("UNUSED_PARAMETER")
- suspend fun animateScrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- animationSpec: AnimationSpec<Float> = spring(),
- initialVelocity: Float = 0f,
- skipPages: Boolean = true,
- ) {
- animateScrollToPage(page = page, pageOffset = pageOffset)
- }
-
- /**
- * Animate (smooth scroll) to the given page to the middle of the viewport.
- *
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param page the page to animate to. Must be between 0 and [pageCount] (inclusive).
- * @param pageOffset the percentage of the page width to offset, from the start of [page]. Must
- * be in the range 0f..1f.
- */
- suspend fun animateScrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- ) {
- requireCurrentPage(page, "page")
- requireCurrentPageOffset(pageOffset, "pageOffset")
- try {
- animationTargetPage = page
-
- if (pageOffset <= 0.005f) {
- // If the offset is (close to) zero, just call animateScrollToItem and we're done
- lazyListState.animateScrollToItem(index = page)
- } else {
- // Else we need to figure out what the offset is in pixels...
-
- var target =
- lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
-
- if (target != null) {
- // If we have access to the target page layout, we can calculate the pixel
- // offset from the size
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (target.size * pageOffset).roundToInt()
- )
- } else {
- // If we don't, we use the current page size as a guide
- val currentSize = currentLayoutPageInfo!!.size
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (currentSize * pageOffset).roundToInt()
- )
-
- // The target should be visible now
- target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page }
-
- if (target.size != currentSize) {
- // If the size we used for calculating the offset differs from the actual
- // target page size, we need to scroll again. This doesn't look great,
- // but there's not much else we can do.
- lazyListState.animateScrollToItem(
- index = page,
- scrollOffset = (target.size * pageOffset).roundToInt()
- )
- }
- }
- }
- } finally {
- // We need to manually call this, as the `animateScrollToItem` call above will happen
- // in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
- // the change. This is especially true when running unit tests.
- onScrollFinished()
- }
- }
-
- /**
- * Instantly brings the item at [page] to the middle of the viewport.
- *
- * Cancels the currently running scroll, if any, and suspends until the cancellation is
- * complete.
- *
- * @param page the page to snap to. Must be between 0 and [pageCount] (inclusive).
- */
- suspend fun scrollToPage(
- @IntRange(from = 0) page: Int,
- @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
- ) {
- requireCurrentPage(page, "page")
- requireCurrentPageOffset(pageOffset, "pageOffset")
- try {
- animationTargetPage = page
-
- // First scroll to the given page. It will now be laid out at offset 0
- lazyListState.scrollToItem(index = page)
-
- // If we have a start spacing, we need to offset (scroll) by that too
- if (pageOffset > 0.0001f) {
- scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } }
- }
- } finally {
- // We need to manually call this, as the `scroll` call above will happen in 1 frame,
- // which is usually too fast for the LaunchedEffect in Pager to detect the change.
- // This is especially true when running unit tests.
- onScrollFinished()
- }
- }
-
- internal fun onScrollFinished() {
- // Then update the current page to our layout page
- currentPage = currentLayoutPageInfo?.index ?: 0
- // Clear the animation target page
- animationTargetPage = null
- }
-
- override suspend fun scroll(
- scrollPriority: MutatePriority,
- block: suspend ScrollScope.() -> Unit
- ) = lazyListState.scroll(scrollPriority, block)
-
- override fun dispatchRawDelta(delta: Float): Float {
- return lazyListState.dispatchRawDelta(delta)
- }
-
- override val isScrollInProgress: Boolean
- get() = lazyListState.isScrollInProgress
-
- override fun toString(): String =
- "PagerState(" +
- "pageCount=$pageCount, " +
- "currentPage=$currentPage, " +
- "currentPageOffset=$currentPageOffset" +
- ")"
-
- private fun requireCurrentPage(value: Int, name: String) {
- if (pageCount == 0) {
- require(value == 0) { "$name must be 0 when pageCount is 0" }
- } else {
- require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" }
- }
- }
-
- private fun requireCurrentPageOffset(value: Float, name: String) {
- if (pageCount == 0) {
- require(value == 0f) { "$name must be 0f when pageCount is 0" }
- } else {
- require(value in 0f..1f) { "$name must be >= 0 and <= 1" }
- }
- }
-
- companion object {
- /** The default [Saver] implementation for [PagerState]. */
- val Saver: Saver<PagerState, *> =
- listSaver(
- save = {
- listOf<Any>(
- it.currentPage,
- )
- },
- restore = {
- PagerState(
- currentPage = it[0] as Int,
- )
- }
- )
- }
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt
deleted file mode 100644
index 98140295306a..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.pager
-
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.AnimationState
-import androidx.compose.animation.core.DecayAnimationSpec
-import androidx.compose.animation.core.animateDecay
-import androidx.compose.animation.core.animateTo
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.animation.core.spring
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ScrollScope
-import androidx.compose.foundation.lazy.LazyListItemInfo
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import kotlin.math.abs
-
-/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */
-internal object SnappingFlingBehaviorDefaults {
- /** TODO */
- val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f)
-}
-
-/**
- * Create and remember a snapping [FlingBehavior] to be used with [LazyListState].
- *
- * @param lazyListState The [LazyListState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- *
- * TODO: move this to a new module and make it public
- */
-@Composable
-internal fun rememberSnappingFlingBehavior(
- lazyListState: LazyListState,
- decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
- snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
-): SnappingFlingBehavior =
- remember(lazyListState, decayAnimationSpec, snapAnimationSpec) {
- SnappingFlingBehavior(
- lazyListState = lazyListState,
- decayAnimationSpec = decayAnimationSpec,
- snapAnimationSpec = snapAnimationSpec,
- )
- }
-
-/**
- * A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via
- * [rememberSnappingFlingBehavior].
- *
- * @param lazyListState The [LazyListState] to update.
- * @param decayAnimationSpec The decay animation spec to use for decayed flings.
- * @param snapAnimationSpec The animation spec to use when snapping.
- */
-internal class SnappingFlingBehavior(
- private val lazyListState: LazyListState,
- private val decayAnimationSpec: DecayAnimationSpec<Float>,
- private val snapAnimationSpec: AnimationSpec<Float>,
-) : FlingBehavior {
- /** The target item index for any on-going animations. */
- var animationTarget: Int? by mutableStateOf(null)
- private set
-
- override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
- val itemInfo = currentItemInfo ?: return initialVelocity
-
- // If the decay fling can scroll past the current item, fling with decay
- return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) {
- performDecayFling(initialVelocity, itemInfo)
- } else {
- // Otherwise we 'spring' to current/next item
- performSpringFling(
- index =
- when {
- // If the velocity is greater than 1 item per second (velocity is px/s),
- // spring
- // in the relevant direction
- initialVelocity > itemInfo.size -> {
- (itemInfo.index + 1).coerceAtMost(
- lazyListState.layoutInfo.totalItemsCount - 1
- )
- }
- initialVelocity < -itemInfo.size -> itemInfo.index
- // If the velocity is 0 (or less than the size of the item), spring to
- // whichever item is closest to the snap point
- itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1
- else -> itemInfo.index
- },
- initialVelocity = initialVelocity,
- )
- }
- }
-
- private suspend fun ScrollScope.performDecayFling(
- initialVelocity: Float,
- startItem: LazyListItemInfo,
- ): Float {
- val index =
- when {
- initialVelocity > 0 -> startItem.index + 1
- else -> startItem.index
- }
- val forward = index > (currentItemInfo?.index ?: return initialVelocity)
-
- // Update the animationTarget
- animationTarget = index
-
- var velocityLeft = initialVelocity
- var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity,
- )
- .animateDecay(decayAnimationSpec) {
- val delta = value - lastValue
- val consumed = scrollBy(delta)
- lastValue = value
- velocityLeft = this.velocity
-
- val current = currentItemInfo
- if (current == null) {
- cancelAnimation()
- return@animateDecay
- }
-
- if (
- !forward &&
- (current.index < index || current.index == index && current.offset >= 0)
- ) {
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (
- forward &&
- (current.index > index || current.index == index && current.offset <= 0)
- ) {
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (abs(delta - consumed) > 0.5f) {
- // avoid rounding errors and stop if anything is unconsumed
- cancelAnimation()
- }
- }
- animationTarget = null
- return velocityLeft
- }
-
- private suspend fun ScrollScope.performSpringFling(
- index: Int,
- scrollOffset: Int = 0,
- initialVelocity: Float = 0f,
- ): Float {
- // If we don't have a current layout, we can't snap
- val initialItem = currentItemInfo ?: return initialVelocity
-
- val forward = index > initialItem.index
- // We add 10% on to the size of the current item, to compensate for any item spacing, etc
- val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f
-
- // Update the animationTarget
- animationTarget = index
-
- var velocityLeft = initialVelocity
- var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = initialVelocity,
- )
- .animateTo(
- targetValue = target,
- animationSpec = snapAnimationSpec,
- ) {
- // Springs can overshoot their target, clamp to the desired range
- val coercedValue =
- if (forward) {
- value.coerceAtMost(target)
- } else {
- value.coerceAtLeast(target)
- }
- val delta = coercedValue - lastValue
- val consumed = scrollBy(delta)
- lastValue = coercedValue
- velocityLeft = this.velocity
-
- val current = currentItemInfo
- if (current == null) {
- cancelAnimation()
- return@animateTo
- }
-
- if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) {
- // If we've scrolled to/past the item, stop the animation. We may also need to
- // 'snap back' to the item as we may have scrolled past it
- scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
- cancelAnimation()
- } else if (abs(delta - consumed) > 0.5f) {
- // avoid rounding errors and stop if anything is unconsumed
- cancelAnimation()
- }
- }
- animationTarget = null
- return velocityLeft
- }
-
- private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int {
- return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0
- }
-
- private val currentItemInfo: LazyListItemInfo?
- get() =
- lazyListState.layoutInfo.visibleItemsInfo
- .asSequence()
- .filter { it.offset <= 0 && it.offset + it.size > 0 }
- .lastOrNull()
-}
-
-private fun scrolledPastItem(
- initialVelocity: Float,
- currentItem: LazyListItemInfo,
- targetIndex: Int,
- targetScrollOffset: Int = 0,
-): Boolean {
- return if (initialVelocity > 0) {
- // forward
- currentItem.index > targetIndex ||
- (currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset)
- } else {
- // backwards
- currentItem.index < targetIndex ||
- (currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset)
- }
-}
-
-private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem(
- currentItem: LazyListItemInfo,
- initialVelocity: Float,
-): Boolean {
- val targetValue =
- calculateTargetValue(
- initialValue = currentItem.offset.toFloat(),
- initialVelocity = initialVelocity,
- )
- return when {
- // forward. We add 10% onto the size to cater for any item spacing
- initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f)
- // backwards. We add 10% onto the size to cater for any item spacing
- else -> targetValue >= (currentItem.size * 0.1f)
- }
-}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt b/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
deleted file mode 100644
index 946e77959b1c..000000000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt
+++ /dev/null
@@ -1,849 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.compose.swipeable
-
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.foundation.gestures.DraggableState
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.Saver
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import com.android.compose.swipeable.SwipeableDefaults.AnimationSpec
-import com.android.compose.swipeable.SwipeableDefaults.StandardResistanceFactor
-import com.android.compose.swipeable.SwipeableDefaults.VelocityThreshold
-import com.android.compose.swipeable.SwipeableDefaults.resistanceConfig
-import com.android.compose.ui.util.lerp
-import kotlin.math.PI
-import kotlin.math.abs
-import kotlin.math.sign
-import kotlin.math.sin
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.launch
-
-/**
- * State of the [swipeable] modifier.
- *
- * This contains necessary information about any ongoing swipe or animation and provides methods to
- * change the state either immediately or by starting an animation. To create and remember a
- * [SwipeableState] with the default animation clock, use [rememberSwipeableState].
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- *
- * TODO(b/272311106): this is a fork from material. Unfork it when Swipeable.kt reaches material3.
- */
-@Stable
-open class SwipeableState<T>(
- initialValue: T,
- internal val animationSpec: AnimationSpec<Float> = AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true }
-) {
- /**
- * The current value of the state.
- *
- * If no swipe or animation is in progress, this corresponds to the anchor at which the
- * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds
- * the last anchor at which the [swipeable] was settled before the swipe or animation started.
- */
- var currentValue: T by mutableStateOf(initialValue)
- private set
-
- /** Whether the state is currently animating. */
- var isAnimationRunning: Boolean by mutableStateOf(false)
- private set
-
- /**
- * The current position (in pixels) of the [swipeable].
- *
- * You should use this state to offset your content accordingly. The recommended way is to use
- * `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
- */
- val offset: State<Float>
- get() = offsetState
-
- /** The amount by which the [swipeable] has been swiped past its bounds. */
- val overflow: State<Float>
- get() = overflowState
-
- // Use `Float.NaN` as a placeholder while the state is uninitialised.
- private val offsetState = mutableStateOf(0f)
- private val overflowState = mutableStateOf(0f)
-
- // the source of truth for the "real"(non ui) position
- // basically position in bounds + overflow
- private val absoluteOffset = mutableStateOf(0f)
-
- // current animation target, if animating, otherwise null
- private val animationTarget = mutableStateOf<Float?>(null)
-
- internal var anchors by mutableStateOf(emptyMap<Float, T>())
-
- private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
- snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1)
-
- internal var minBound = Float.NEGATIVE_INFINITY
- internal var maxBound = Float.POSITIVE_INFINITY
-
- internal fun ensureInit(newAnchors: Map<Float, T>) {
- if (anchors.isEmpty()) {
- // need to do initial synchronization synchronously :(
- val initialOffset = newAnchors.getOffset(currentValue)
- requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
- offsetState.value = initialOffset
- absoluteOffset.value = initialOffset
- }
- }
-
- internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) {
- if (oldAnchors.isEmpty()) {
- // If this is the first time that we receive anchors, then we need to initialise
- // the state so we snap to the offset associated to the initial value.
- minBound = newAnchors.keys.minOrNull()!!
- maxBound = newAnchors.keys.maxOrNull()!!
- val initialOffset = newAnchors.getOffset(currentValue)
- requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
- snapInternalToOffset(initialOffset)
- } else if (newAnchors != oldAnchors) {
- // If we have received new anchors, then the offset of the current value might
- // have changed, so we need to animate to the new offset. If the current value
- // has been removed from the anchors then we animate to the closest anchor
- // instead. Note that this stops any ongoing animation.
- minBound = Float.NEGATIVE_INFINITY
- maxBound = Float.POSITIVE_INFINITY
- val animationTargetValue = animationTarget.value
- // if we're in the animation already, let's find it a new home
- val targetOffset =
- if (animationTargetValue != null) {
- // first, try to map old state to the new state
- val oldState = oldAnchors[animationTargetValue]
- val newState = newAnchors.getOffset(oldState)
- // return new state if exists, or find the closes one among new anchors
- newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
- } else {
- // we're not animating, proceed by finding the new anchors for an old value
- val actualOldValue = oldAnchors[offset.value]
- val value = if (actualOldValue == currentValue) currentValue else actualOldValue
- newAnchors.getOffset(value)
- ?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!!
- }
- try {
- animateInternalToOffset(targetOffset, animationSpec)
- } catch (c: CancellationException) {
- // If the animation was interrupted for any reason, snap as a last resort.
- snapInternalToOffset(targetOffset)
- } finally {
- currentValue = newAnchors.getValue(targetOffset)
- minBound = newAnchors.keys.minOrNull()!!
- maxBound = newAnchors.keys.maxOrNull()!!
- }
- }
- }
-
- internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
-
- internal var velocityThreshold by mutableStateOf(0f)
-
- internal var resistance: ResistanceConfig? by mutableStateOf(null)
-
- internal val draggableState = DraggableState {
- val newAbsolute = absoluteOffset.value + it
- val clamped = newAbsolute.coerceIn(minBound, maxBound)
- val overflow = newAbsolute - clamped
- val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
- offsetState.value = clamped + resistanceDelta
- overflowState.value = overflow
- absoluteOffset.value = newAbsolute
- }
-
- private suspend fun snapInternalToOffset(target: Float) {
- draggableState.drag { dragBy(target - absoluteOffset.value) }
- }
-
- private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
- draggableState.drag {
- var prevValue = absoluteOffset.value
- animationTarget.value = target
- isAnimationRunning = true
- try {
- Animatable(prevValue).animateTo(target, spec) {
- dragBy(this.value - prevValue)
- prevValue = this.value
- }
- } finally {
- animationTarget.value = null
- isAnimationRunning = false
- }
- }
- }
-
- /**
- * The target value of the state.
- *
- * If a swipe is in progress, this is the value that the [swipeable] would animate to if the
- * swipe finished. If an animation is running, this is the target value of that animation.
- * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
- */
- val targetValue: T
- get() {
- // TODO(calintat): Track current velocity (b/149549482) and use that here.
- val target =
- animationTarget.value
- ?: computeTarget(
- offset = offset.value,
- lastValue = anchors.getOffset(currentValue) ?: offset.value,
- anchors = anchors.keys,
- thresholds = thresholds,
- velocity = 0f,
- velocityThreshold = Float.POSITIVE_INFINITY
- )
- return anchors[target] ?: currentValue
- }
-
- /**
- * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
- *
- * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
- */
- val progress: SwipeProgress<T>
- get() {
- val bounds = findBounds(offset.value, anchors.keys)
- val from: T
- val to: T
- val fraction: Float
- when (bounds.size) {
- 0 -> {
- from = currentValue
- to = currentValue
- fraction = 1f
- }
- 1 -> {
- from = anchors.getValue(bounds[0])
- to = anchors.getValue(bounds[0])
- fraction = 1f
- }
- else -> {
- val (a, b) =
- if (direction > 0f) {
- bounds[0] to bounds[1]
- } else {
- bounds[1] to bounds[0]
- }
- from = anchors.getValue(a)
- to = anchors.getValue(b)
- fraction = (offset.value - a) / (b - a)
- }
- }
- return SwipeProgress(from, to, fraction)
- }
-
- /**
- * The direction in which the [swipeable] is moving, relative to the current [currentValue].
- *
- * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
- * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
- */
- val direction: Float
- get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f
-
- /**
- * Set the state without any animation and suspend until it's set
- *
- * @param targetValue The new target value to set [currentValue] to.
- */
- suspend fun snapTo(targetValue: T) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- val targetOffset = anchors.getOffset(targetValue)
- requireNotNull(targetOffset) { "The target value must have an associated anchor." }
- snapInternalToOffset(targetOffset)
- currentValue = targetValue
- }
- }
-
- /**
- * Set the state to the target value by starting an animation.
- *
- * @param targetValue The new value to animate to.
- * @param anim The animation that will be used to animate to the new value.
- */
- suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- try {
- val targetOffset = anchors.getOffset(targetValue)
- requireNotNull(targetOffset) { "The target value must have an associated anchor." }
- animateInternalToOffset(targetOffset, anim)
- } finally {
- val endOffset = absoluteOffset.value
- val endValue =
- anchors
- // fighting rounding error once again, anchor should be as close as 0.5
- // pixels
- .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
- .values
- .firstOrNull()
- ?: currentValue
- currentValue = endValue
- }
- }
- }
-
- /**
- * Perform fling with settling to one of the anchors which is determined by the given
- * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided
- * since it will settle at the anchor.
- *
- * In general cases, [swipeable] flings by itself when being swiped. This method is to be used
- * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
- * trigger settling fling when the child scroll container reaches the bound.
- *
- * @param velocity velocity to fling and settle with
- * @return the reason fling ended
- */
- suspend fun performFling(velocity: Float) {
- latestNonEmptyAnchorsFlow.collect { anchors ->
- val lastAnchor = anchors.getOffset(currentValue)!!
- val targetValue =
- computeTarget(
- offset = offset.value,
- lastValue = lastAnchor,
- anchors = anchors.keys,
- thresholds = thresholds,
- velocity = velocity,
- velocityThreshold = velocityThreshold
- )
- val targetState = anchors[targetValue]
- if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
- // If the user vetoed the state change, rollback to the previous state.
- else animateInternalToOffset(lastAnchor, animationSpec)
- }
- }
-
- /**
- * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable]
- * gesture flow.
- *
- * Note: This method performs generic drag and it won't settle to any particular anchor, *
- * leaving swipeable in between anchors. When done dragging, [performFling] must be called as
- * well to ensure swipeable will settle at the anchor.
- *
- * In general cases, [swipeable] drags by itself when being swiped. This method is to be used
- * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to
- * force drag when the child scroll container reaches the bound.
- *
- * @param delta delta in pixels to drag by
- * @return the amount of [delta] consumed
- */
- fun performDrag(delta: Float): Float {
- val potentiallyConsumed = absoluteOffset.value + delta
- val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
- val deltaToConsume = clamped - absoluteOffset.value
- if (abs(deltaToConsume) > 0) {
- draggableState.dispatchRawDelta(deltaToConsume)
- }
- return deltaToConsume
- }
-
- companion object {
- /** The default [Saver] implementation for [SwipeableState]. */
- fun <T : Any> Saver(
- animationSpec: AnimationSpec<Float>,
- confirmStateChange: (T) -> Boolean
- ) =
- Saver<SwipeableState<T>, T>(
- save = { it.currentValue },
- restore = { SwipeableState(it, animationSpec, confirmStateChange) }
- )
- }
-}
-
-/**
- * Collects information about the ongoing swipe or animation in [swipeable].
- *
- * To access this information, use [SwipeableState.progress].
- *
- * @param from The state corresponding to the anchor we are moving away from.
- * @param to The state corresponding to the anchor we are moving towards.
- * @param fraction The fraction that the current position represents between [from] and [to]. Must
- * be between `0` and `1`.
- */
-@Immutable
-class SwipeProgress<T>(
- val from: T,
- val to: T,
- /*@FloatRange(from = 0.0, to = 1.0)*/
- val fraction: Float
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is SwipeProgress<*>) return false
-
- if (from != other.from) return false
- if (to != other.to) return false
- if (fraction != other.fraction) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = from?.hashCode() ?: 0
- result = 31 * result + (to?.hashCode() ?: 0)
- result = 31 * result + fraction.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
- }
-}
-
-/**
- * Create and [remember] a [SwipeableState] with the default animation clock.
- *
- * @param initialValue The initial value of the state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
- */
-@Composable
-fun <T : Any> rememberSwipeableState(
- initialValue: T,
- animationSpec: AnimationSpec<Float> = AnimationSpec,
- confirmStateChange: (newValue: T) -> Boolean = { true }
-): SwipeableState<T> {
- return rememberSaveable(
- saver =
- SwipeableState.Saver(
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- ) {
- SwipeableState(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- }
-}
-
-/**
- * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.:
- * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value.
- * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the
- * [value] will be notified to update their state to the new value of the [SwipeableState] by
- * invoking [onValueChange]. If the owner does not update their state to the provided value for
- * some reason, then the [SwipeableState] will perform a rollback to the previous, correct value.
- */
-@Composable
-internal fun <T : Any> rememberSwipeableStateFor(
- value: T,
- onValueChange: (T) -> Unit,
- animationSpec: AnimationSpec<Float> = AnimationSpec
-): SwipeableState<T> {
- val swipeableState = remember {
- SwipeableState(
- initialValue = value,
- animationSpec = animationSpec,
- confirmStateChange = { true }
- )
- }
- val forceAnimationCheck = remember { mutableStateOf(false) }
- LaunchedEffect(value, forceAnimationCheck.value) {
- if (value != swipeableState.currentValue) {
- swipeableState.animateTo(value)
- }
- }
- DisposableEffect(swipeableState.currentValue) {
- if (value != swipeableState.currentValue) {
- onValueChange(swipeableState.currentValue)
- forceAnimationCheck.value = !forceAnimationCheck.value
- }
- onDispose {}
- }
- return swipeableState
-}
-
-/**
- * Enable swipe gestures between a set of predefined states.
- *
- * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that
- * this map cannot be empty and cannot have two anchors mapped to the same state.
- *
- * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe
- * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`).
- * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableState] will also be updated to the state corresponding to the
- * new anchor. The target anchor is calculated based on the provided positional [thresholds].
- *
- * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
- * past these bounds, a resistance effect will be applied by default. The amount of resistance at
- * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
- *
- * For an example of a [swipeable] with three states, see:
- *
- * @param T The type of the state.
- * @param state The state of the [swipeable].
- * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
- * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
- * used to determine which state to animate to when swiping stops. This is represented as a lambda
- * that takes two states and returns the threshold between them in the form of a
- * [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction.
- * @param orientation The orientation in which the [swipeable] can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
- * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe
- * will behave like bottom to top, and a left to right swipe will behave like right to left.
- * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
- * [Modifier.draggable].
- * @param resistance Controls how much resistance will be applied when swiping past the bounds.
- * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in
- * order to animate to the next state, even if the positional [thresholds] have not been reached.
- * @sample androidx.compose.material.samples.SwipeableSample
- */
-fun <T> Modifier.swipeable(
- state: SwipeableState<T>,
- anchors: Map<Float, T>,
- orientation: Orientation,
- enabled: Boolean = true,
- reverseDirection: Boolean = false,
- interactionSource: MutableInteractionSource? = null,
- thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
- resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
- velocityThreshold: Dp = VelocityThreshold
-) =
- composed(
- inspectorInfo =
- debugInspectorInfo {
- name = "swipeable"
- properties["state"] = state
- properties["anchors"] = anchors
- properties["orientation"] = orientation
- properties["enabled"] = enabled
- properties["reverseDirection"] = reverseDirection
- properties["interactionSource"] = interactionSource
- properties["thresholds"] = thresholds
- properties["resistance"] = resistance
- properties["velocityThreshold"] = velocityThreshold
- }
- ) {
- require(anchors.isNotEmpty()) { "You must have at least one anchor." }
- require(anchors.values.distinct().count() == anchors.size) {
- "You cannot have two anchors mapped to the same state."
- }
- val density = LocalDensity.current
- state.ensureInit(anchors)
- LaunchedEffect(anchors, state) {
- val oldAnchors = state.anchors
- state.anchors = anchors
- state.resistance = resistance
- state.thresholds = { a, b ->
- val from = anchors.getValue(a)
- val to = anchors.getValue(b)
- with(thresholds(from, to)) { density.computeThreshold(a, b) }
- }
- with(density) { state.velocityThreshold = velocityThreshold.toPx() }
- state.processNewAnchors(oldAnchors, anchors)
- }
-
- Modifier.draggable(
- orientation = orientation,
- enabled = enabled,
- reverseDirection = reverseDirection,
- interactionSource = interactionSource,
- startDragImmediately = state.isAnimationRunning,
- onDragStopped = { velocity -> launch { state.performFling(velocity) } },
- state = state.draggableState
- )
- }
-
-/**
- * Interface to compute a threshold between two anchors/states in a [swipeable].
- *
- * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
- */
-@Stable
-interface ThresholdConfig {
- /** Compute the value of the threshold (in pixels), once the values of the anchors are known. */
- fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
-}
-
-/**
- * A fixed threshold will be at an [offset] away from the first anchor.
- *
- * @param offset The offset (in dp) that the threshold will be at.
- */
-@Immutable
-data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
- override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
- return fromValue + offset.toPx() * sign(toValue - fromValue)
- }
-}
-
-/**
- * A fractional threshold will be at a [fraction] of the way between the two anchors.
- *
- * @param fraction The fraction (between 0 and 1) that the threshold will be at.
- */
-@Immutable
-data class FractionalThreshold(
- /*@FloatRange(from = 0.0, to = 1.0)*/
- private val fraction: Float
-) : ThresholdConfig {
- override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
- return lerp(fromValue, toValue, fraction)
- }
-}
-
-/**
- * Specifies how resistance is calculated in [swipeable].
- *
- * There are two things needed to calculate resistance: the resistance basis determines how much
- * overflow will be consumed to achieve maximum resistance, and the resistance factor determines the
- * amount of resistance (the larger the resistance factor, the stronger the resistance).
- *
- * The resistance basis is usually either the size of the component which [swipeable] is applied to,
- * or the distance between the minimum and maximum anchors. For a constructor in which the
- * resistance basis defaults to the latter, consider using [resistanceConfig].
- *
- * You may specify different resistance factors for each bound. Consider using one of the default
- * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has
- * run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this
- * right now. Also, you can set either factor to 0 to disable resistance at that bound.
- *
- * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
- * @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be
- * negative.
- * @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be
- * negative.
- */
-@Immutable
-class ResistanceConfig(
- /*@FloatRange(from = 0.0, fromInclusive = false)*/
- val basis: Float,
- /*@FloatRange(from = 0.0)*/
- val factorAtMin: Float = StandardResistanceFactor,
- /*@FloatRange(from = 0.0)*/
- val factorAtMax: Float = StandardResistanceFactor
-) {
- fun computeResistance(overflow: Float): Float {
- val factor = if (overflow < 0) factorAtMin else factorAtMax
- if (factor == 0f) return 0f
- val progress = (overflow / basis).coerceIn(-1f, 1f)
- return basis / factor * sin(progress * PI.toFloat() / 2)
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is ResistanceConfig) return false
-
- if (basis != other.basis) return false
- if (factorAtMin != other.factorAtMin) return false
- if (factorAtMax != other.factorAtMax) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = basis.hashCode()
- result = 31 * result + factorAtMin.hashCode()
- result = 31 * result + factorAtMax.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
- }
-}
-
-/**
- * Given an offset x and a set of anchors, return a list of anchors:
- * 1. [ ] if the set of anchors is empty,
- * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is
- * x rounded to the exact value of the matching anchor,
- * 3. [ min ] if min is the minimum anchor and x < min,
- * 4. [ max ] if max is the maximum anchor and x > max, or
- * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
- */
-private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> {
- // Find the anchors the target lies between with a little bit of rounding error.
- val a = anchors.filter { it <= offset + 0.001 }.maxOrNull()
- val b = anchors.filter { it >= offset - 0.001 }.minOrNull()
-
- return when {
- a == null ->
- // case 1 or 3
- listOfNotNull(b)
- b == null ->
- // case 4
- listOf(a)
- a == b ->
- // case 2
- // Can't return offset itself here since it might not be exactly equal
- // to the anchor, despite being considered an exact match.
- listOf(a)
- else ->
- // case 5
- listOf(a, b)
- }
-}
-
-private fun computeTarget(
- offset: Float,
- lastValue: Float,
- anchors: Set<Float>,
- thresholds: (Float, Float) -> Float,
- velocity: Float,
- velocityThreshold: Float
-): Float {
- val bounds = findBounds(offset, anchors)
- return when (bounds.size) {
- 0 -> lastValue
- 1 -> bounds[0]
- else -> {
- val lower = bounds[0]
- val upper = bounds[1]
- if (lastValue <= offset) {
- // Swiping from lower to upper (positive).
- if (velocity >= velocityThreshold) {
- return upper
- } else {
- val threshold = thresholds(lower, upper)
- if (offset < threshold) lower else upper
- }
- } else {
- // Swiping from upper to lower (negative).
- if (velocity <= -velocityThreshold) {
- return lower
- } else {
- val threshold = thresholds(upper, lower)
- if (offset > threshold) upper else lower
- }
- }
- }
- }
-}
-
-private fun <T> Map<Float, T>.getOffset(state: T): Float? {
- return entries.firstOrNull { it.value == state }?.key
-}
-
-/** Contains useful defaults for [swipeable] and [SwipeableState]. */
-object SwipeableDefaults {
- /** The default animation used by [SwipeableState]. */
- val AnimationSpec = SpringSpec<Float>()
-
- /** The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. */
- val VelocityThreshold = 125.dp
-
- /** A stiff resistance factor which indicates that swiping isn't available right now. */
- const val StiffResistanceFactor = 20f
-
- /** A standard resistance factor which indicates that the user has run out of things to see. */
- const val StandardResistanceFactor = 10f
-
- /**
- * The default resistance config used by [swipeable].
- *
- * This returns `null` if there is one anchor. If there are at least two anchors, it returns a
- * [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
- */
- fun resistanceConfig(
- anchors: Set<Float>,
- factorAtMin: Float = StandardResistanceFactor,
- factorAtMax: Float = StandardResistanceFactor
- ): ResistanceConfig? {
- return if (anchors.size <= 1) {
- null
- } else {
- val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
- ResistanceConfig(basis, factorAtMin, factorAtMax)
- }
- }
-}
-
-// temp default nested scroll connection for swipeables which desire as an opt in
-// revisit in b/174756744 as all types will have their own specific connection probably
-internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection
- get() =
- object : NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- val delta = available.toFloat()
- return if (delta < 0 && source == NestedScrollSource.Drag) {
- performDrag(delta).toOffset()
- } else {
- Offset.Zero
- }
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- return if (source == NestedScrollSource.Drag) {
- performDrag(available.toFloat()).toOffset()
- } else {
- Offset.Zero
- }
- }
-
- override suspend fun onPreFling(available: Velocity): Velocity {
- val toFling = Offset(available.x, available.y).toFloat()
- return if (toFling < 0 && offset.value > minBound) {
- performFling(velocity = toFling)
- // since we go to the anchor with tween settling, consume all for the best UX
- available
- } else {
- Velocity.Zero
- }
- }
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- performFling(velocity = Offset(available.x, available.y).toFloat())
- return available
- }
-
- private fun Float.toOffset(): Offset = Offset(0f, this)
-
- private fun Offset.toFloat(): Float = this.y
- }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt
new file mode 100644
index 000000000000..741f00d9f19b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.ui.util
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+/**
+ * Iterates through a [List] using the index and calls [action] for each item. This does not
+ * allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
+ contract { callsInPlace(action) }
+ for (index in indices) {
+ val item = get(index)
+ action(item)
+ }
+}
+
+/**
+ * Returns a list containing the results of applying the given [transform] function to each element
+ * in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
+ contract { callsInPlace(transform) }
+ val target = ArrayList<R>(size)
+ fastForEach { target += transform(it) }
+ return target
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
index c1defb722077..eb1a634ff491 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
@@ -17,11 +17,10 @@
package com.android.compose.ui.util
+import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
import kotlin.math.roundToLong
-// TODO(b/272311106): this is a fork from material. Unfork it when MathHelpers.kt reaches material3.
-
/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
fun lerp(start: Float, stop: Float, fraction: Float): Float {
return (1 - fraction) * start + fraction * stop
@@ -36,3 +35,11 @@ fun lerp(start: Int, stop: Int, fraction: Float): Int {
fun lerp(start: Long, stop: Long, fraction: Float): Long {
return start + ((stop - start) * fraction.toDouble()).roundToLong()
}
+
+/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
+fun lerp(start: IntSize, stop: IntSize, fraction: Float): IntSize {
+ return IntSize(
+ lerp(start.width, stop.width, fraction),
+ lerp(start.height, stop.height, fraction)
+ )
+}
diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp
index 6119e96e5bac..eb80da726c31 100644
--- a/packages/SystemUI/compose/core/tests/Android.bp
+++ b/packages/SystemUI/compose/core/tests/Android.bp
@@ -42,6 +42,8 @@ android_test {
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui-test-junit4",
"androidx.compose.ui_ui-test-manifest",
+
+ "truth-prebuilt",
],
kotlincflags: ["-Xjvm-default=enable"],
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
new file mode 100644
index 000000000000..04b3f8a1dfe7
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ObservableTransitionStateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testObservableTransitionState() = runTest {
+ val state = SceneTransitionLayoutState(TestScenes.SceneA)
+
+ // Collect the current observable state into [observableState].
+ // TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be
+ // reused by non-SystemUI testing code.
+ var observableState: ObservableTransitionState? = null
+ backgroundScope.launch {
+ state.observableTransitionState().collect { observableState = it }
+ }
+
+ fun observableState(): ObservableTransitionState {
+ runCurrent()
+ return observableState!!
+ }
+
+ fun ObservableTransitionState.Transition.progress(): Float {
+ var lastProgress = -1f
+ backgroundScope.launch { progress.collect { lastProgress = it } }
+ runCurrent()
+ return lastProgress
+ }
+
+ rule.testTransition(
+ from = TestScenes.SceneA,
+ to = TestScenes.SceneB,
+ transitionLayout = { currentScene, onChangeScene ->
+ SceneTransitionLayout(
+ currentScene,
+ onChangeScene,
+ EmptyTestTransitions,
+ state = state,
+ ) {
+ scene(TestScenes.SceneA) {}
+ scene(TestScenes.SceneB) {}
+ }
+ }
+ ) {
+ before {
+ assertThat(observableState())
+ .isEqualTo(ObservableTransitionState.Idle(TestScenes.SceneA))
+ }
+ at(0) {
+ val state = observableState()
+ assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java)
+ assertThat((state as ObservableTransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(state.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(state.progress()).isEqualTo(0f)
+ }
+ at(TestTransitionDuration / 2) {
+ val state = observableState()
+ assertThat((state as ObservableTransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(state.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(state.progress()).isEqualTo(0.5f)
+ }
+ after {
+ assertThat(observableState())
+ .isEqualTo(ObservableTransitionState.Idle(TestScenes.SceneB))
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
new file mode 100644
index 000000000000..8bd654585f29
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.activity.ComponentActivity
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.subjects.DpOffsetSubject
+import com.android.compose.test.subjects.assertThat
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SceneTransitionLayoutTest {
+ companion object {
+ private val LayoutSize = 300.dp
+ }
+
+ private var currentScene by mutableStateOf(TestScenes.SceneA)
+ private val layoutState = SceneTransitionLayoutState(currentScene)
+
+ // We use createAndroidComposeRule() here and not createComposeRule() because we need an
+ // activity for testBack().
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ /** The content under test. */
+ @Composable
+ private fun TestContent() {
+ SceneTransitionLayout(
+ currentScene,
+ { currentScene = it },
+ EmptyTestTransitions,
+ state = layoutState,
+ modifier = Modifier.size(LayoutSize),
+ ) {
+ scene(
+ TestScenes.SceneA,
+ userActions = mapOf(Back to TestScenes.SceneB),
+ ) {
+ Box(Modifier.fillMaxSize()) {
+ SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
+ Text("SceneA")
+ }
+ }
+ scene(TestScenes.SceneB) {
+ Box(Modifier.fillMaxSize()) {
+ SharedFoo(
+ size = 100.dp,
+ childOffset = 50.dp,
+ Modifier.align(Alignment.TopStart),
+ )
+ Text("SceneB")
+ }
+ }
+ scene(TestScenes.SceneC) {
+ Box(Modifier.fillMaxSize()) {
+ SharedFoo(
+ size = 150.dp,
+ childOffset = 100.dp,
+ Modifier.align(Alignment.BottomStart),
+ )
+ Text("SceneC")
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
+ Box(
+ modifier
+ .size(size)
+ .background(Color.Red)
+ .element(TestElements.Foo)
+ .testTag(TestElements.Foo.name)
+ ) {
+ // Offset the single child of Foo by some animated shared offset.
+ val offset by animateSharedDpAsState(childOffset, TestValues.Value1, TestElements.Foo)
+
+ Box(
+ Modifier.offset {
+ val pxOffset = offset.roundToPx()
+ IntOffset(pxOffset, pxOffset)
+ }
+ .size(30.dp)
+ .background(Color.Blue)
+ .testTag(TestElements.Bar.name)
+ )
+ }
+ }
+
+ @Test
+ fun testOnlyCurrentSceneIsDisplayed() {
+ rule.setContent { TestContent() }
+
+ // Only scene A is displayed.
+ rule.onNodeWithText("SceneA").assertIsDisplayed()
+ rule.onNodeWithText("SceneB").assertDoesNotExist()
+ rule.onNodeWithText("SceneC").assertDoesNotExist()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Change to scene B. Only that scene is displayed.
+ currentScene = TestScenes.SceneB
+ rule.onNodeWithText("SceneA").assertDoesNotExist()
+ rule.onNodeWithText("SceneB").assertIsDisplayed()
+ rule.onNodeWithText("SceneC").assertDoesNotExist()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun testBack() {
+ rule.setContent { TestContent() }
+
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ rule.activity.onBackPressed()
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun testTransitionState() {
+ rule.setContent { TestContent() }
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // We will advance the clock manually.
+ rule.mainClock.autoAdvance = false
+
+ // Change the current scene. Until composition is triggered, this won't change the layout
+ // state.
+ currentScene = TestScenes.SceneB
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // On the next frame, we will recompose because currentScene changed, which will start the
+ // transition (i.e. it will change the transitionState to be a Transition) in a
+ // LaunchedEffect.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ val transition = layoutState.transitionState as TransitionState.Transition
+ assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.progress).isEqualTo(0f)
+
+ // Then, on the next frame, the animator we started gets its initial value and clock
+ // starting time. We are now at progress = 0f.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(0f)
+
+ // The test transition lasts 480ms. 240ms after the start of the transition, we are at
+ // progress = 0.5f.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(0.5f)
+
+ // (240-16) ms later, i.e. one frame before the transition is finished, we are at
+ // progress=(480-16)/480.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2 - 16)
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo((TestTransitionDuration - 16) / 480f)
+
+ // one frame (16ms) later, the transition is finished and we are in the idle state in scene
+ // B.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun testSharedElement() {
+ rule.setContent { TestContent() }
+
+ // In scene A, the shared element SharedFoo() is at the top end of the layout and has a size
+ // of 50.dp.
+ var sharedFoo = rule.onNodeWithTag(TestElements.Foo.name, useUnmergedTree = true)
+ sharedFoo.assertWidthIsEqualTo(50.dp)
+ sharedFoo.assertHeightIsEqualTo(50.dp)
+ sharedFoo.assertPositionInRootIsEqualTo(
+ expectedTop = 0.dp,
+ expectedLeft = LayoutSize - 50.dp,
+ )
+
+ // The shared offset of the single child of SharedFoo() is 0dp in scene A.
+ assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo)).isEqualTo(DpOffset(0.dp, 0.dp))
+
+ // Pause animations to test the state mid-transition.
+ rule.mainClock.autoAdvance = false
+
+ // Go to scene B and let the animation start. See [testLayoutState()] and
+ // [androidx.compose.ui.test.MainTestClock] to understand why we need to advance the clock
+ // by 2 frames to be at the start of the animation.
+ currentScene = TestScenes.SceneB
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Advance to the middle of the animation.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
+ // We need to use onAllNodesWithTag().onFirst() here given that shared elements are
+ // composed and laid out in both scenes (but drawn only in one).
+ sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst()
+
+ // In scene B, foo is at the top start (x = 0, y = 0) of the layout and has a size of
+ // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
+ // use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is
+ // going to (x = 0, y = 0), so the offset should now be half what it was.
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(0.5f)
+ sharedFoo.assertWidthIsEqualTo(75.dp)
+ sharedFoo.assertHeightIsEqualTo(75.dp)
+ sharedFoo.assertPositionInRootIsEqualTo(
+ expectedTop = 0.dp,
+ expectedLeft = (LayoutSize - 50.dp) / 2
+ )
+
+ // The shared offset of the single child of SharedFoo() is 50dp in scene B and 0dp in Scene
+ // A, so it should be 25dp now.
+ assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo))
+ .isWithin(DpOffsetSubject.DefaultTolerance)
+ .of(DpOffset(25.dp, 25.dp))
+
+ // Animate to scene C, let the animation start then go to the middle of the transition.
+ currentScene = TestScenes.SceneC
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
+ // In Scene C, foo is at the bottom start of the layout and has a size of 150.dp. The
+ // transition scene B => scene C is using a FastOutSlowIn interpolator.
+ val interpolatedProgress = FastOutSlowInEasing.transform(0.5f)
+ val expectedTop = (LayoutSize - 150.dp) * interpolatedProgress
+ val expectedLeft = 0.dp
+ val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
+
+ sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst()
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(interpolatedProgress)
+ sharedFoo.assertWidthIsEqualTo(expectedSize)
+ sharedFoo.assertHeightIsEqualTo(expectedSize)
+ sharedFoo.assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+
+ // The shared offset of the single child of SharedFoo() is 50dp in scene B and 100dp in
+ // Scene C.
+ val expectedOffset = 50.dp + (100.dp - 50.dp) * interpolatedProgress
+ assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo))
+ .isWithin(DpOffsetSubject.DefaultTolerance)
+ .of(DpOffset(expectedOffset, expectedOffset))
+
+ // Go back to scene A. This should happen instantly (once the animation started, i.e. after
+ // 2 frames) given that we use a snap() animation spec.
+ currentScene = TestScenes.SceneA
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ }
+
+ private fun SemanticsNodeInteraction.offsetRelativeTo(
+ other: SemanticsNodeInteraction,
+ ): DpOffset {
+ val node = fetchSemanticsNode()
+ val bounds = node.boundsInRoot
+ val otherBounds = other.fetchSemanticsNode().boundsInRoot
+ return with(node.layoutInfo.density) {
+ DpOffset(
+ x = (bounds.left - otherBounds.left).toDp(),
+ y = (bounds.top - otherBounds.top).toDp(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
new file mode 100644
index 000000000000..cb2607a2845e
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SwipeToSceneTest {
+ companion object {
+ private val LayoutWidth = 200.dp
+ private val LayoutHeight = 400.dp
+
+ /** The middle of the layout, in pixels. */
+ private val Density.middle: Offset
+ get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
+ }
+
+ private var currentScene by mutableStateOf(TestScenes.SceneA)
+ private val layoutState = SceneTransitionLayoutState(currentScene)
+
+ @get:Rule val rule = createComposeRule()
+
+ /** The content under test. */
+ @Composable
+ private fun TestContent() {
+ SceneTransitionLayout(
+ currentScene,
+ { currentScene = it },
+ EmptyTestTransitions,
+ state = layoutState,
+ modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.name),
+ ) {
+ scene(
+ TestScenes.SceneA,
+ userActions =
+ mapOf(
+ Swipe.Left to TestScenes.SceneB,
+ Swipe.Down to TestScenes.SceneC,
+ ),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ scene(
+ TestScenes.SceneB,
+ userActions = mapOf(Swipe.Right to TestScenes.SceneA),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ scene(
+ TestScenes.SceneC,
+ userActions = mapOf(Swipe.Down to TestScenes.SceneA),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ @Test
+ fun testDragWithPositionalThreshold() {
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Drag left (i.e. from right to left) by 55dp. We pick 55dp here because 56dp is the
+ // positional threshold from which we commit the gesture.
+ rule.onRoot().performTouchInput {
+ down(middle)
+
+ // We use a high delay so that the velocity of the gesture is slow (otherwise it would
+ // commit the gesture, even if we are below the positional threshold).
+ moveBy(Offset(-55.dp.toPx() - touchSlop, 0f), delayMillis = 1_000)
+ }
+
+ // We should be at a progress = 55dp / LayoutWidth given that we use the layout size in
+ // the gesture axis as swipe distance.
+ var transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+ // Release the finger. We should now be animating back to A (currentScene = SceneA) given
+ // that 55dp < positional threshold.
+ rule.onRoot().performTouchInput { up() }
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+ // Wait for the animation to finish. We should now be in scene A.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Now we do the same but vertically and with a drag distance of 56dp, which is >=
+ // positional threshold.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, 56.dp.toPx() + touchSlop), delayMillis = 1_000)
+ }
+
+ // Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
+
+ // Release the finger. We should now be animating to C (currentScene = SceneC) given
+ // that 56dp >= positional threshold.
+ rule.onRoot().performTouchInput { up() }
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
+
+ // Wait for the animation to finish. We should now be in scene C.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
+
+ @Test
+ fun testSwipeWithVelocityThreshold() {
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Swipe left (i.e. from right to left) using a velocity of 124 dp/s. We pick 124 dp/s here
+ // because 125 dp/s is the velocity threshold from which we commit the gesture. We also use
+ // a swipe distance < 56dp, the positional threshold, to make sure that we don't commit
+ // the gesture because of a large enough swipe distance.
+ rule.onRoot().performTouchInput {
+ swipeWithVelocity(
+ start = middle,
+ end = middle - Offset(55.dp.toPx() + touchSlop, 0f),
+ endVelocity = 124.dp.toPx(),
+ )
+ }
+
+ // We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity
+ // threshold.
+ var transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+ // Wait for the animation to finish. We should now be in scene A.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Now we do the same but vertically and with a swipe velocity of 126dp, which is >
+ // velocity threshold. Note that in theory we could have used 125 dp (= velocity threshold)
+ // but it doesn't work reliably with how swipeWithVelocity() computes move events to get to
+ // the target velocity, probably because of float rounding errors.
+ rule.onRoot().performTouchInput {
+ swipeWithVelocity(
+ start = middle,
+ end = middle + Offset(0f, 55.dp.toPx() + touchSlop),
+ endVelocity = 126.dp.toPx(),
+ )
+ }
+
+ // We should be animating to C (currentScene = SceneC).
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutHeight)
+
+ // Wait for the animation to finish. We should now be in scene C.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt
new file mode 100644
index 000000000000..275149a05abf
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+
+@DslMarker annotation class TransitionTestDsl
+
+@TransitionTestDsl
+interface TransitionTestBuilder {
+ /**
+ * Assert on the state of the layout before the transition starts.
+ *
+ * This should be called maximum once, before [at] or [after] is called.
+ */
+ fun before(builder: TransitionTestAssertionScope.() -> Unit)
+
+ /**
+ * Assert on the state of the layout during the transition at [timestamp].
+ *
+ * This should be called after [before] is called and before [after] is called. Successive calls
+ * to [at] must be called with increasing [timestamp].
+ *
+ * Important: [timestamp] must be a multiple of 16 (the duration of a frame on the JVM/Android).
+ * There is no intermediary state between `t` and `t + 16` , so testing transitions outside of
+ * `t = 0`, `t = 16`, `t = 32`, etc does not make sense.
+ */
+ fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit)
+
+ /**
+ * Assert on the state of the layout after the transition finished.
+ *
+ * This should be called maximum once, after [before] or [at] is called.
+ */
+ fun after(builder: TransitionTestAssertionScope.() -> Unit)
+}
+
+@TransitionTestDsl
+interface TransitionTestAssertionScope {
+ /** Assert on [element]. */
+ fun onElement(element: ElementKey): SemanticsNodeInteraction
+}
+
+/**
+ * Test the transition between [fromSceneContent] and [toSceneContent] at different points in time.
+ *
+ * @sample com.android.compose.animation.scene.transformation.TranslateTest
+ */
+fun ComposeContentTestRule.testTransition(
+ fromSceneContent: @Composable SceneScope.() -> Unit,
+ toSceneContent: @Composable SceneScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ layoutModifier: Modifier = Modifier,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ testTransition(
+ from = TestScenes.SceneA,
+ to = TestScenes.SceneB,
+ transitionLayout = { currentScene, onChangeScene ->
+ SceneTransitionLayout(
+ currentScene,
+ onChangeScene,
+ transitions { from(TestScenes.SceneA, to = TestScenes.SceneB, transition) },
+ layoutModifier.fillMaxSize(),
+ ) {
+ scene(TestScenes.SceneA, content = fromSceneContent)
+ scene(TestScenes.SceneB, content = toSceneContent)
+ }
+ },
+ builder,
+ )
+}
+
+/**
+ * Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different
+ * points in time.
+ */
+fun ComposeContentTestRule.testTransition(
+ from: SceneKey,
+ to: SceneKey,
+ transitionLayout:
+ @Composable
+ (
+ currentScene: SceneKey,
+ onChangeScene: (SceneKey) -> Unit,
+ ) -> Unit,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ val test = transitionTest(builder)
+ val assertionScope =
+ object : TransitionTestAssertionScope {
+ override fun onElement(element: ElementKey): SemanticsNodeInteraction {
+ return this@testTransition.onNodeWithTag(element.name)
+ }
+ }
+
+ var currentScene by mutableStateOf(from)
+ setContent { transitionLayout(currentScene, { currentScene = it }) }
+
+ // Wait for the UI to be idle then test the before state.
+ waitForIdle()
+ test.before(assertionScope)
+
+ // Manually advance the clock to the start of the animation.
+ mainClock.autoAdvance = false
+
+ // Change the current scene.
+ currentScene = to
+
+ // Advance by a frame to trigger recomposition, which will start the transition (i.e. it will
+ // change the transitionState to be a Transition) in a LaunchedEffect.
+ mainClock.advanceTimeByFrame()
+
+ // Advance by another frame so that the animator we started gets its initial value and clock
+ // starting time. We are now at progress = 0f.
+ mainClock.advanceTimeByFrame()
+ waitForIdle()
+
+ // Test the assertions at specific points in time.
+ test.timestamps.forEach { tsAssertion ->
+ if (tsAssertion.timestampDelta > 0L) {
+ mainClock.advanceTimeBy(tsAssertion.timestampDelta)
+ waitForIdle()
+ }
+
+ tsAssertion.assertion(assertionScope)
+ }
+
+ // Go to the end state and test it.
+ mainClock.autoAdvance = true
+ waitForIdle()
+ test.after(assertionScope)
+}
+
+private fun transitionTest(builder: TransitionTestBuilder.() -> Unit): TransitionTest {
+ // Collect the assertion lambdas in [TransitionTest]. Note that the ordering is forced by the
+ // builder, e.g. `before {}` must be called before everything else, then `at {}` (in increasing
+ // order of timestamp), then `after {}`. That way the test code is run with the same order as it
+ // is written, to avoid confusion.
+
+ val impl =
+ object : TransitionTestBuilder {
+ var before: (TransitionTestAssertionScope.() -> Unit)? = null
+ var after: (TransitionTestAssertionScope.() -> Unit)? = null
+ val timestamps = mutableListOf<TimestampAssertion>()
+
+ private var currentTimestamp = 0L
+
+ override fun before(builder: TransitionTestAssertionScope.() -> Unit) {
+ check(before == null) { "before {} must be called maximum once" }
+ check(after == null) { "before {} must be called before after {}" }
+ check(timestamps.isEmpty()) { "before {} must be called before at(...) {}" }
+
+ before = builder
+ }
+
+ override fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit) {
+ check(after == null) { "at(...) {} must be called before after {}" }
+ check(timestamp >= currentTimestamp) {
+ "at(...) must be called with timestamps in increasing order"
+ }
+ check(timestamp % 16 == 0L) {
+ "timestamp must be a multiple of the frame time (16ms)"
+ }
+
+ val delta = timestamp - currentTimestamp
+ currentTimestamp = timestamp
+
+ timestamps.add(TimestampAssertion(delta, builder))
+ }
+
+ override fun after(builder: TransitionTestAssertionScope.() -> Unit) {
+ check(after == null) { "after {} must be called maximum once" }
+ after = builder
+ }
+ }
+ .apply(builder)
+
+ return TransitionTest(
+ before = impl.before ?: {},
+ timestamps = impl.timestamps,
+ after = impl.after ?: {},
+ )
+}
+
+private class TransitionTest(
+ val before: TransitionTestAssertionScope.() -> Unit,
+ val after: TransitionTestAssertionScope.() -> Unit,
+ val timestamps: List<TimestampAssertion>,
+)
+
+private class TimestampAssertion(
+ val timestampDelta: Long,
+ val assertion: TransitionTestAssertionScope.() -> Unit,
+)
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt
new file mode 100644
index 000000000000..83572620c88a
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+
+/** Scenes keys that can be reused by tests. */
+object TestScenes {
+ val SceneA = SceneKey("SceneA")
+ val SceneB = SceneKey("SceneB")
+ val SceneC = SceneKey("SceneC")
+}
+
+/** Element keys that can be reused by tests. */
+object TestElements {
+ val Foo = ElementKey("Foo")
+ val Bar = ElementKey("Bar")
+}
+
+/** Value keys that can be reused by tests. */
+object TestValues {
+ val Value1 = ValueKey("Value1")
+}
+
+// We use a transition duration of 480ms here because it is a multiple of 16, the time of a frame in
+// C JVM/Android. Doing so allows us for instance to test the state at progress = 0.5f given that t
+// = 240ms is also a multiple of 16.
+val TestTransitionDuration = 480L
+
+/** A definition of empty transitions between [TestScenes], using different animation specs. */
+val EmptyTestTransitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ spec = tween(durationMillis = TestTransitionDuration.toInt(), easing = LinearEasing)
+ }
+
+ from(TestScenes.SceneB, to = TestScenes.SceneC) {
+ spec = tween(durationMillis = TestTransitionDuration.toInt(), easing = FastOutSlowInEasing)
+ }
+
+ from(TestScenes.SceneC, to = TestScenes.SceneA) { spec = snap() }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
new file mode 100644
index 000000000000..8ef6757d33bd
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchoredSizeTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testAnchoredSizeEnter() {
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo)) },
+ toSceneContent = {
+ Box(Modifier.size(50.dp, 50.dp).element(TestElements.Foo))
+ Box(Modifier.size(200.dp, 60.dp).element(TestElements.Bar))
+ },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredSize(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar is entering. It starts at the same size as Foo in scene A in and scales to its
+ // final size in scene B.
+ before { onElement(TestElements.Bar).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+ at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
+ at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
+ at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
+ at(64) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
+ after { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
+ }
+ }
+
+ @Test
+ fun testAnchoredSizeExit() {
+ rule.testTransition(
+ fromSceneContent = {
+ Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo))
+ Box(Modifier.size(100.dp, 100.dp).element(TestElements.Bar))
+ },
+ toSceneContent = { Box(Modifier.size(200.dp, 60.dp).element(TestElements.Foo)) },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredSize(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar is leaving. It starts at 100dp x 100dp in scene A and is scaled to 200dp x 60dp,
+ // the size of Foo in scene B.
+ before { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+ at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+ at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
+ at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
+ at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
+ after { onElement(TestElements.Bar).assertDoesNotExist() }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt
new file mode 100644
index 000000000000..d1205e727cf9
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchoredTranslateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testAnchoredTranslateExit() {
+ rule.testTransition(
+ fromSceneContent = {
+ Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar))
+ },
+ toSceneContent = { Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo)) },
+ transition = {
+ // Anchor Bar to Foo, which is moving from (10dp, 50dp) to (30dp, 10dp).
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredTranslate(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar moves by (20dp, -40dp), like Foo.
+ before { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(25.dp, 30.dp) }
+ at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(30.dp, 20.dp) }
+ at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(35.dp, 10.dp) }
+ after { onElement(TestElements.Bar).assertDoesNotExist() }
+ }
+ }
+
+ @Test
+ fun testAnchoredTranslateEnter() {
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo)) },
+ toSceneContent = {
+ Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo))
+ Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar))
+ },
+ transition = {
+ // Anchor Bar to Foo, which is moving from (10dp, 50dp) to (30dp, 10dp).
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredTranslate(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar moves by (20dp, -40dp), like Foo.
+ before { onElement(TestElements.Bar).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(0.dp, 80.dp) }
+ at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(5.dp, 70.dp) }
+ at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(10.dp, 60.dp) }
+ at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(15.dp, 50.dp) }
+ at(64) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ after { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
new file mode 100644
index 000000000000..2a27763f1d5c
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.TransitionTestBuilder
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EdgeTranslateTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ private fun testEdgeTranslate(
+ edge: Edge,
+ startsOutsideLayoutBounds: Boolean,
+ builder: TransitionTestBuilder.() -> Unit,
+ ) {
+ rule.testTransition(
+ // The layout under test is 300dp x 300dp.
+ layoutModifier = Modifier.size(300.dp),
+ fromSceneContent = {},
+ toSceneContent = {
+ // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)
+ Box(Modifier.fillMaxSize()) {
+ Box(Modifier.size(100.dp).element(TestElements.Foo).align(Alignment.Center))
+ }
+ },
+ transition = {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(TestElements.Foo, edge, startsOutsideLayoutBounds)
+ },
+ builder = builder,
+ )
+ }
+
+ @Test
+ fun testEntersFromTop_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Top, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, (-100).dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 0.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromTop_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Top, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 0.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 50.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromBottom_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Bottom, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 300.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 200.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromBottom_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Bottom, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 200.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 150.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromLeft_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Left, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo((-100).dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromLeft_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Left, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromRight_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Right, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(300.dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(200.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromRight_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Right, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(200.dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(150.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt
new file mode 100644
index 000000000000..384355ca951f
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ScaleSizeTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testScaleSize() {
+ rule.testTransition(
+ fromSceneContent = {},
+ toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ scaleSize(TestElements.Foo, width = 2f, height = 0.5f)
+ },
+ ) {
+ // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the
+ // transition so it starts at 200dp x 50dp.
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(200.dp, 50.dp) }
+ at(16) { onElement(TestElements.Foo).assertSizeIsEqualTo(175.dp, 62.5.dp) }
+ at(32) { onElement(TestElements.Foo).assertSizeIsEqualTo(150.dp, 75.dp) }
+ at(48) { onElement(TestElements.Foo).assertSizeIsEqualTo(125.dp, 87.5.dp) }
+ at(64) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt
new file mode 100644
index 000000000000..1d559fd6bd8a
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TranslateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testTranslateExit() {
+ rule.testTransition(
+ fromSceneContent = {
+ // Foo is at (10dp, 50dp) and is exiting.
+ Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ },
+ toSceneContent = {},
+ transition = {
+ // Foo is translated by (20dp, -40dp) during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(TestElements.Foo, x = 20.dp, y = (-40).dp)
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(15.dp, 40.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 30.dp) }
+ at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(25.dp, 20.dp) }
+ after { onElement(TestElements.Foo).assertDoesNotExist() }
+ }
+ }
+
+ @Test
+ fun testTranslateEnter() {
+ rule.testTransition(
+ fromSceneContent = {},
+ toSceneContent = {
+ // Foo is entering to (10dp, 50dp)
+ Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ },
+ transition = {
+ // Foo is translated from (10dp, 50) + (20dp, -40dp) during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(TestElements.Foo, x = 20.dp, y = (-40).dp)
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(30.dp, 10.dp) }
+ at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(25.dp, 20.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 30.dp) }
+ at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(15.dp, 40.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt
index c2eaf72a841a..fbd1b512c50a 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt
@@ -1,11 +1,11 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.multishade.shared.math
+package com.android.compose.test
-import androidx.annotation.VisibleForTesting
-import kotlin.math.abs
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.unit.Dp
-/** Returns `true` if this [Float] is within [epsilon] of `0`. */
-fun Float.isZero(epsilon: Float = EPSILON): Boolean {
- return abs(this) < epsilon
+fun SemanticsNodeInteraction.assertSizeIsEqualTo(expectedWidth: Dp, expectedHeight: Dp) {
+ assertWidthIsEqualTo(expectedWidth)
+ assertHeightIsEqualTo(expectedHeight)
}
-
-@VisibleForTesting private const val EPSILON = 0.0001f
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
new file mode 100644
index 000000000000..bf7bf98878e6
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.test.subjects
+
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
+
+/** Assert on a [DpOffset]. */
+fun assertThat(dpOffset: DpOffset): DpOffsetSubject {
+ return assertAbout(DpOffsetSubject.dpOffsets()).that(dpOffset)
+}
+
+/** A Truth subject to assert on [DpOffset] with some tolerance. Inspired by FloatSubject. */
+class DpOffsetSubject(
+ metadata: FailureMetadata,
+ private val actual: DpOffset,
+) : Subject(metadata, actual) {
+ fun isWithin(tolerance: Dp): TolerantDpOffsetComparison {
+ return object : TolerantDpOffsetComparison {
+ override fun of(expected: DpOffset) {
+ actual.x.assertIsEqualTo(expected.x, "offset.x", tolerance)
+ actual.y.assertIsEqualTo(expected.y, "offset.y", tolerance)
+ }
+ }
+ }
+
+ interface TolerantDpOffsetComparison {
+ fun of(expected: DpOffset)
+ }
+
+ companion object {
+ val DefaultTolerance = Dp(.5f)
+
+ fun dpOffsets() =
+ Factory<DpOffsetSubject, DpOffset> { metadata, actual ->
+ DpOffsetSubject(metadata, actual)
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 82fe3f265384..609ea90e9159 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
@@ -53,14 +51,6 @@ object ComposeFacade : BaseComposeFacade {
throwComposeUnavailableError()
}
- override fun createMultiShadeView(
- context: Context,
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- ): View {
- throwComposeUnavailableError()
- }
-
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 7926f9224347..0ee88b90bcc4 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -23,8 +23,6 @@ import androidx.activity.compose.setContent
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
-import com.android.systemui.multishade.ui.composable.MultiShade
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -34,7 +32,6 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
@@ -60,23 +57,6 @@ object ComposeFacade : BaseComposeFacade {
}
}
- override fun createMultiShadeView(
- context: Context,
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- ): View {
- return ComposeView(context).apply {
- setContent {
- PlatformTheme {
- MultiShade(
- viewModel = viewModel,
- clock = clock,
- )
- }
- }
- }
- }
-
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index b3d2e350ed50..64227b8c5f2a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -43,10 +43,10 @@ import androidx.compose.ui.res.integerResource
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
+import com.android.compose.modifiers.thenIf
import com.android.internal.R
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
-import com.android.systemui.compose.modifiers.thenIf
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
@@ -228,43 +228,45 @@ internal fun PatternBouncer(
}
}
) {
- // Draw lines between dots.
- selectedDots.forEachIndexed { index, dot ->
- if (index > 0) {
- val previousDot = selectedDots[index - 1]
- val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
- val startLerp = 1 - lineFadeOutAnimationProgress
- val from = pixelOffset(previousDot, spacing, verticalOffset)
- val to = pixelOffset(dot, spacing, verticalOffset)
- val lerpedFrom =
- Offset(
- x = from.x + (to.x - from.x) * startLerp,
- y = from.y + (to.y - from.y) * startLerp,
+ if (isAnimationEnabled) {
+ // Draw lines between dots.
+ selectedDots.forEachIndexed { index, dot ->
+ if (index > 0) {
+ val previousDot = selectedDots[index - 1]
+ val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
+ val startLerp = 1 - lineFadeOutAnimationProgress
+ val from = pixelOffset(previousDot, spacing, verticalOffset)
+ val to = pixelOffset(dot, spacing, verticalOffset)
+ val lerpedFrom =
+ Offset(
+ x = from.x + (to.x - from.x) * startLerp,
+ y = from.y + (to.y - from.y) * startLerp,
+ )
+ drawLine(
+ start = lerpedFrom,
+ end = to,
+ cap = StrokeCap.Round,
+ alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
)
- drawLine(
- start = lerpedFrom,
- end = to,
- cap = StrokeCap.Round,
- alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ }
}
- }
- // Draw the line between the most recently-selected dot and the input pointer position.
- inputPosition?.let { lineEnd ->
- currentDot?.let { dot ->
- val from = pixelOffset(dot, spacing, verticalOffset)
- val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
- drawLine(
- start = from,
- end = lineEnd,
- cap = StrokeCap.Round,
- alpha = lineAlpha(spacing, lineLength),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ // Draw the line between the most recently-selected dot and the input pointer position.
+ inputPosition?.let { lineEnd ->
+ currentDot?.let { dot ->
+ val from = pixelOffset(dot, spacing, verticalOffset)
+ val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
+ drawLine(
+ start = from,
+ end = lineEnd,
+ cap = StrokeCap.Round,
+ alpha = lineAlpha(spacing, lineLength),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
+ )
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index 85178bc26a62..ec6e5eda264e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -14,76 +14,51 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalAnimationApi::class, ExperimentalAnimationGraphicsApi::class)
-
package com.android.systemui.bouncer.ui.composable
import android.view.HapticFeedbackConstants
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.animation.core.Transition
-import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
-import androidx.compose.animation.graphics.res.animatedVectorResource
-import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
-import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid
-import com.android.internal.R.id.image
+import com.android.compose.modifiers.thenIf
import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
-import com.android.systemui.bouncer.ui.viewmodel.EnteredKey
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.compose.modifiers.thenIf
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlinx.coroutines.async
@@ -110,147 +85,6 @@ internal fun PinBouncer(
}
@Composable
-private fun PinInputDisplay(viewModel: PinBouncerViewModel) {
- val currentPinEntries: List<EnteredKey> by viewModel.pinEntries.collectAsState()
-
- // visiblePinEntries keeps pins removed from currentPinEntries in the composition until their
- // disappear-animation completed. The list is sorted by the natural ordering of EnteredKey,
- // which is guaranteed to produce the original edit order, since the model only modifies entries
- // at the end.
- val visiblePinEntries = remember { SnapshotStateList<EnteredKey>() }
- currentPinEntries.forEach {
- val index = visiblePinEntries.binarySearch(it)
- if (index < 0) {
- val insertionPoint = -(index + 1)
- visiblePinEntries.add(insertionPoint, it)
- }
- }
-
- Row(
- modifier =
- Modifier.heightIn(min = entryShapeSize)
- // Pins overflowing horizontally should still be shown as scrolling.
- .wrapContentSize(unbounded = true),
- ) {
- visiblePinEntries.forEachIndexed { index, entry ->
- key(entry) {
- val visibility = remember {
- MutableTransitionState<EntryVisibility>(EntryVisibility.Hidden)
- }
- visibility.targetState =
- when {
- currentPinEntries.isEmpty() && visiblePinEntries.size > 1 ->
- EntryVisibility.BulkHidden(index, visiblePinEntries.size)
- currentPinEntries.contains(entry) -> EntryVisibility.Shown
- else -> EntryVisibility.Hidden
- }
-
- val shape = viewModel.pinShapes.getShape(entry.sequenceNumber)
- PinInputEntry(shape, updateTransition(visibility, label = "Pin Entry $entry"))
-
- LaunchedEffect(entry) {
- // Remove entry from visiblePinEntries once the hide transition completed.
- snapshotFlow {
- visibility.currentState == visibility.targetState &&
- visibility.targetState != EntryVisibility.Shown
- }
- .collect { isRemoved ->
- if (isRemoved) {
- visiblePinEntries.remove(entry)
- }
- }
- }
- }
- }
- }
-}
-
-private sealed class EntryVisibility {
- object Shown : EntryVisibility()
-
- object Hidden : EntryVisibility()
-
- /**
- * Same as [Hidden], but applies when multiple entries are hidden simultaneously, without
- * collapsing during the hide.
- */
- data class BulkHidden(val staggerIndex: Int, val totalEntryCount: Int) : EntryVisibility()
-}
-
-@Composable
-private fun PinInputEntry(shapeResourceId: Int, transition: Transition<EntryVisibility>) {
- // spec: http://shortn/_DEhE3Xl2bi
- val dismissStaggerDelayMs = 33
- val dismissDurationMs = 450
- val expansionDurationMs = 250
- val shapeCollapseDurationMs = 200
-
- val animatedEntryWidth by
- transition.animateDp(
- transitionSpec = {
- when (val target = targetState) {
- is EntryVisibility.BulkHidden ->
- // only collapse horizontal space once all entries are removed
- snap(dismissDurationMs + dismissStaggerDelayMs * target.totalEntryCount)
- else -> tween(expansionDurationMs, easing = Easings.Standard)
- }
- },
- label = "entry space"
- ) { state ->
- if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
- }
-
- val animatedShapeSize by
- transition.animateDp(
- transitionSpec = {
- when {
- EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> {
- // The AVD contains the entry transition.
- snap()
- }
- targetState is EntryVisibility.BulkHidden -> {
- val target = targetState as EntryVisibility.BulkHidden
- tween(
- dismissDurationMs,
- delayMillis = target.staggerIndex * dismissStaggerDelayMs,
- easing = Easings.Legacy,
- )
- }
- else -> tween(shapeCollapseDurationMs, easing = Easings.StandardDecelerate)
- }
- },
- label = "shape size"
- ) { state ->
- if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
- }
-
- val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
- Layout(
- content = {
- val image = AnimatedImageVector.animatedVectorResource(shapeResourceId)
- var atEnd by remember { mutableStateOf(false) }
- Image(
- painter = rememberAnimatedVectorPainter(image, atEnd),
- contentDescription = null,
- contentScale = ContentScale.Crop,
- colorFilter = ColorFilter.tint(dotColor),
- )
- LaunchedEffect(Unit) { atEnd = true }
- }
- ) { measurables, _ ->
- val shapeSizePx = animatedShapeSize.roundToPx()
- val placeable = measurables.single().measure(Constraints.fixed(shapeSizePx, shapeSizePx))
-
- layout(animatedEntryWidth.roundToPx(), entryShapeSize.roundToPx()) {
- placeable.place(
- ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
- ((entryShapeSize - animatedShapeSize) / 2f).roundToPx()
- )
- }
- }
-}
-
-@Composable
private fun PinPad(viewModel: PinBouncerViewModel) {
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
@@ -512,8 +346,6 @@ private suspend fun showFailureAnimation(
}
}
-private val entryShapeSize = 30.dp
-
private val pinButtonSize = 84.dp
private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize
private const val pinButtonErrorShrinkMs = 50
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
new file mode 100644
index 000000000000..77065cfdeb76
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalAnimationGraphicsApi::class)
+
+package com.android.systemui.bouncer.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.toMutableStateList
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.Easings
+import com.android.keyguard.PinShapeAdapter
+import com.android.systemui.R
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.joinAll
+import kotlinx.coroutines.launch
+
+@Composable
+fun PinInputDisplay(viewModel: PinBouncerViewModel) {
+ val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsState()
+ val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes)
+
+ // The display comes in two different flavors:
+ // 1) hinting: shows a circle (◦) per expected pin input, and dot (●) per entered digit.
+ // This has a fixed width, and uses two distinct types of AVDs to animate the addition and
+ // removal of digits.
+ // 2) regular, shows a dot (●) per entered digit.
+ // This grows/shrinks as digits are added deleted. Uses the same type of AVDs to animate the
+ // addition of digits, but simply center-shrinks the dot (●) shape to zero to animate the
+ // removal.
+ // Because of all these differences, there are two separate implementations, rather than
+ // unifying into a single, more complex implementation.
+
+ when (val length = hintedPinLength) {
+ null -> RegularPinInputDisplay(viewModel, shapeAnimations)
+ else -> HintingPinInputDisplay(viewModel, shapeAnimations, length)
+ }
+}
+
+/**
+ * A pin input display that shows a placeholder circle (◦) for every digit in the pin not yet
+ * entered.
+ *
+ * Used for auto-confirmed pins of a specific length, see design: http://shortn/_jS8kPzQ7QV
+ */
+@Composable
+private fun HintingPinInputDisplay(
+ viewModel: PinBouncerViewModel,
+ shapeAnimations: ShapeAnimations,
+ hintedPinLength: Int,
+) {
+ val pinInput: PinInputViewModel by viewModel.pinInput.collectAsState()
+ // [ClearAll] marker pointing at the beginning of the current pin input.
+ // When a new [ClearAll] token is added to the [pinInput], the clear-all animation is played
+ // and the marker is advanced manually to the most recent marker. See LaunchedEffect below.
+ var currentClearAll by remember { mutableStateOf(pinInput.mostRecentClearAll()) }
+ // The length of the pin currently entered by the user.
+ val currentPinLength = pinInput.getDigits(currentClearAll).size
+
+ // The animated vector drawables for each of the [hintedPinLength] slots.
+ // The first [currentPinLength] drawables end in a dot (●) shape, the remaining drawables up to
+ // [hintedPinLength] end in the circle (◦) shape.
+ // This list is re-generated upon each pin entry, it is modelled as a [MutableStateList] to
+ // allow the clear-all animation to replace the shapes asynchronously, see LaunchedEffect below.
+ // Note that when a [ClearAll] token is added to the input (and the clear-all animation plays)
+ // the [currentPinLength] does not change; the [pinEntryDrawable] is remembered until the
+ // clear-all animation finishes and the [currentClearAll] state is manually advanced.
+ val pinEntryDrawable =
+ remember(currentPinLength) {
+ buildList {
+ repeat(currentPinLength) { add(shapeAnimations.getShapeToDot(it)) }
+ repeat(hintedPinLength - currentPinLength) { add(shapeAnimations.dotToCircle) }
+ }
+ .toMutableStateList()
+ }
+
+ val mostRecentClearAll = pinInput.mostRecentClearAll()
+ // Whenever a new [ClearAll] marker is added to the input, the clear-all animation needs to
+ // be played.
+ LaunchedEffect(mostRecentClearAll) {
+ if (currentClearAll == mostRecentClearAll) {
+ // Except during the initial composition.
+ return@LaunchedEffect
+ }
+
+ // Staggered replace of all dot (●) shapes with an animation from dot (●) to circle (◦).
+ for (index in 0 until hintedPinLength) {
+ if (!shapeAnimations.isDotShape(pinEntryDrawable[index])) break
+
+ pinEntryDrawable[index] = shapeAnimations.dotToCircle
+ delay(shapeAnimations.dismissStaggerDelay)
+ }
+
+ // Once the animation is done, start processing the next pin input again.
+ currentClearAll = mostRecentClearAll
+ }
+
+ // During the initial composition, do not play the [pinEntryDrawable] animations. This prevents
+ // the dot (●) to circle (◦) animation when the empty display becomes first visible, and a
+ // superfluous shape to dot (●) animation after for example device rotation.
+ var playAnimation by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { playAnimation = true }
+
+ val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+ Row(modifier = Modifier.heightIn(min = shapeAnimations.shapeSize)) {
+ pinEntryDrawable.forEachIndexed { index, drawable ->
+ // Key the loop by [index] and [drawable], so that updating a shape drawable at the same
+ // index will play the new animation (by remembering a new [atEnd]).
+ key(index, drawable) {
+ // [rememberAnimatedVectorPainter] requires a `atEnd` boolean to switch from `false`
+ // to `true` for the animation to play. This animation is suppressed when
+ // playAnimation is false, always rendering the end-state of the animation.
+ var atEnd by remember { mutableStateOf(!playAnimation) }
+ LaunchedEffect(Unit) { atEnd = true }
+
+ Image(
+ painter = rememberAnimatedVectorPainter(drawable, atEnd),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ colorFilter = ColorFilter.tint(dotColor),
+ )
+ }
+ }
+ }
+}
+
+/**
+ * A pin input that shows a dot (●) for each entered pin, horizontally centered and growing /
+ * shrinking as more digits are entered and deleted.
+ *
+ * Used for pin input when the pin length is not hinted, see design http://shortn/_wNP7SrBD78
+ */
+@Composable
+private fun RegularPinInputDisplay(
+ viewModel: PinBouncerViewModel,
+ shapeAnimations: ShapeAnimations,
+) {
+ // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from
+ // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove
+ // animation, thus the composable to be removed has to remain in the composition until fully
+ // disappeared (see `prune` launched effect below)
+ val pinInputRow = remember(shapeAnimations) { PinInputRow(shapeAnimations) }
+
+ // Processed `viewModel.pinInput` updates and applies them to [pinDigitShapes]
+ LaunchedEffect(viewModel.pinInput, pinInputRow) {
+ // Initial setup: capture the most recent [ClearAll] marker and create the visuals for the
+ // existing digits (if any) without animation..
+ var currentClearAll =
+ with(viewModel.pinInput.value) {
+ val initialClearAll = mostRecentClearAll()
+ pinInputRow.setDigits(getDigits(initialClearAll))
+ initialClearAll
+ }
+
+ viewModel.pinInput.collect { input ->
+ // Process additions and removals of pins within the current input block.
+ pinInputRow.updateDigits(input.getDigits(currentClearAll), scope = this@LaunchedEffect)
+
+ val mostRecentClearAll = input.mostRecentClearAll()
+ if (currentClearAll != mostRecentClearAll) {
+ // A new [ClearAll] token is added to the [input], play the clear-all animation
+ pinInputRow.playClearAllAnimation()
+
+ // Animation finished, advance manually to the next marker.
+ currentClearAll = mostRecentClearAll
+ }
+ }
+ }
+
+ LaunchedEffect(pinInputRow) {
+ // Prunes unused VisiblePinEntries once they are no longer visible.
+ snapshotFlow { pinInputRow.hasUnusedEntries() }
+ .collect { hasUnusedEntries ->
+ if (hasUnusedEntries) {
+ pinInputRow.prune()
+ }
+ }
+ }
+
+ pinInputRow.Content()
+}
+
+private class PinInputRow(
+ val shapeAnimations: ShapeAnimations,
+) {
+ private val entries = mutableStateListOf<PinInputEntry>()
+
+ @Composable
+ fun Content() {
+ Row(
+ modifier =
+ Modifier.heightIn(min = shapeAnimations.shapeSize)
+ // Pins overflowing horizontally should still be shown as scrolling.
+ .wrapContentSize(unbounded = true),
+ ) {
+ entries.forEach { entry -> key(entry.digit) { entry.Content() } }
+ }
+ }
+
+ /**
+ * Replaces all current [PinInputEntry] composables with new instances for each digit.
+ *
+ * Does not play the entry expansion animation.
+ */
+ fun setDigits(digits: List<Digit>) {
+ entries.clear()
+ entries.addAll(digits.map { PinInputEntry(it, shapeAnimations) })
+ }
+
+ /**
+ * Adds [PinInputEntry] composables for new digits and plays an entry animation, and starts the
+ * exit animation for digits not in [updated] anymore.
+ *
+ * The function return immediately, playing the animations in the background.
+ *
+ * Removed entries have to be [prune]d once the exit animation completes, [hasUnusedEntries] can
+ * be used in a [SnapshotFlow] to discover when its time to do so.
+ */
+ fun updateDigits(updated: List<Digit>, scope: CoroutineScope) {
+ val incoming = updated.minus(entries.map { it.digit }.toSet()).toList()
+ val outgoing = entries.filterNot { entry -> updated.any { entry.digit == it } }.toList()
+
+ entries.addAll(
+ incoming.map {
+ PinInputEntry(it, shapeAnimations).apply { scope.launch { animateAppearance() } }
+ }
+ )
+
+ outgoing.forEach { entry -> scope.launch { entry.animateRemoval() } }
+
+ entries.sortWith(compareBy { it.digit })
+ }
+
+ /**
+ * Plays a staggered remove animation, and upon completion removes the [PinInputEntry]
+ * composables.
+ *
+ * This function returns once the animation finished playing and the entries are removed.
+ */
+ suspend fun playClearAllAnimation() = coroutineScope {
+ val entriesToRemove = entries.toList()
+ entriesToRemove
+ .mapIndexed { index, entry ->
+ launch {
+ delay(shapeAnimations.dismissStaggerDelay * index)
+ entry.animateClearAllCollapse()
+ }
+ }
+ .joinAll()
+
+ // Remove all [PinInputEntry] composables for which the staggered remove animation was
+ // played. Note that up to now, each PinInputEntry still occupied the full width.
+ entries.removeAll(entriesToRemove)
+ }
+
+ /**
+ * Whether there are [PinInputEntry] that can be removed from the composition since they were
+ * fully animated out.
+ */
+ fun hasUnusedEntries(): Boolean {
+ return entries.any { it.isUnused }
+ }
+
+ /** Remove all no longer visible [PinInputEntry]s from the composition. */
+ fun prune() {
+ entries.removeAll { it.isUnused }
+ }
+}
+
+private class PinInputEntry(
+ val digit: Digit,
+ val shapeAnimations: ShapeAnimations,
+) {
+ private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber)
+ // horizontal space occupied, used to shift contents as individual digits are animated in/out
+ private val entryWidth =
+ Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Width of pin ($digit)")
+ // intrinsic width and height of the shape, used to collapse the shape during exit animations.
+ private val shapeSize =
+ Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Size of pin ($digit)")
+
+ /**
+ * Whether the is fully animated out. When `true`, removing this from the composable won't have
+ * visual effects.
+ */
+ val isUnused: Boolean
+ get() {
+ return entryWidth.targetValue == 0.dp && !entryWidth.isRunning
+ }
+
+ /** Animate the shape appearance by growing the entry width from 0.dp to the intrinsic width. */
+ suspend fun animateAppearance() = coroutineScope {
+ entryWidth.snapTo(0.dp)
+ entryWidth.animateTo(shapeAnimations.shapeSize, shapeAnimations.inputShiftAnimationSpec)
+ }
+
+ /**
+ * Animates shape disappearance by collapsing the shape and occupied horizontal space.
+ *
+ * Once complete, [isUnused] will return `true`.
+ */
+ suspend fun animateRemoval() = coroutineScope {
+ awaitAll(
+ async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) },
+ async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) }
+ )
+ }
+
+ /** Collapses the shape in place, while still holding on to the horizontal space. */
+ suspend fun animateClearAllCollapse() = coroutineScope {
+ shapeSize.animateTo(0.dp, shapeAnimations.clearAllShapeSizeAnimationSpec)
+ }
+
+ @Composable
+ fun Content() {
+ val animatedShapeSize by shapeSize.asState()
+ val animatedEntryWidth by entryWidth.asState()
+
+ val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
+ val shapeHeight = shapeAnimations.shapeSize
+ var atEnd by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { atEnd = true }
+ Image(
+ painter = rememberAnimatedVectorPainter(shape, atEnd),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ colorFilter = ColorFilter.tint(dotColor),
+ modifier =
+ Modifier.layout { measurable, _ ->
+ val shapeSizePx = animatedShapeSize.roundToPx()
+ val placeable = measurable.measure(Constraints.fixed(shapeSizePx, shapeSizePx))
+
+ layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) {
+ placeable.place(
+ ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(),
+ ((shapeHeight - animatedShapeSize) / 2f).roundToPx()
+ )
+ }
+ },
+ )
+ }
+}
+
+/** Animated Vector Drawables used to render the pin input. */
+private class ShapeAnimations(
+ /** Width and height for all the animation images listed here. */
+ val shapeSize: Dp,
+ /** Transitions from the dot (●) to the circle (◦). Used for the hinting pin input only. */
+ val dotToCircle: AnimatedImageVector,
+ /** Each of the animations transition from nothing via a shape to the dot (●). */
+ private val shapesToDot: List<AnimatedImageVector>,
+) {
+ /**
+ * Returns a transition from nothing via shape to the dot (●)., specific to the input position.
+ */
+ fun getShapeToDot(position: Int): AnimatedImageVector {
+ return shapesToDot[position.mod(shapesToDot.size)]
+ }
+
+ /**
+ * Whether the [shapeAnimation] is a image returned by [getShapeToDot], and thus is ending in
+ * the dot (●) shape.
+ *
+ * `false` if the shape's end state is the circle (◦).
+ */
+ fun isDotShape(shapeAnimation: AnimatedImageVector): Boolean {
+ return shapeAnimation != dotToCircle
+ }
+
+ // spec: http://shortn/_DEhE3Xl2bi
+ val dismissStaggerDelay = 33.milliseconds
+ val inputShiftAnimationSpec = tween<Dp>(durationMillis = 250, easing = Easings.Standard)
+ val deleteShapeSizeAnimationSpec =
+ tween<Dp>(durationMillis = 200, easing = Easings.StandardDecelerate)
+ val clearAllShapeSizeAnimationSpec = tween<Dp>(durationMillis = 450, easing = Easings.Legacy)
+}
+
+@Composable
+private fun rememberShapeAnimations(pinShapes: PinShapeAdapter): ShapeAnimations {
+ // NOTE: `animatedVectorResource` does remember the returned AnimatedImageVector.
+ val dotToCircle = AnimatedImageVector.animatedVectorResource(R.drawable.pin_dot_delete_avd)
+ val shapesToDot = pinShapes.shapes.map { AnimatedImageVector.animatedVectorResource(it) }
+ val shapeSize = dimensionResource(R.dimen.password_shape_size)
+
+ return remember(dotToCircle, shapesToDot, shapeSize) {
+ ShapeAnimations(shapeSize, dotToCircle, shapesToDot)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
deleted file mode 100644
index 99fe26ce1f3b..000000000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.composable
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.gestures.detectVerticalDragGestures
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.unit.IntSize
-import com.android.systemui.R
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
-import com.android.systemui.notifications.ui.composable.Notifications
-import com.android.systemui.qs.footer.ui.compose.QuickSettings
-import com.android.systemui.statusbar.ui.composable.StatusBar
-import com.android.systemui.util.time.SystemClock
-
-@Composable
-fun MultiShade(
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- modifier: Modifier = Modifier,
-) {
- val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState()
- val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState()
-
- // TODO(b/273298030): find a different way to get the height constraint from its parent.
- BoxWithConstraints(modifier = modifier) {
- val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() }
-
- Scrim(
- modifier = Modifier.fillMaxSize(),
- remoteTouch = viewModel::onScrimTouched,
- alpha = { scrimAlpha },
- isScrimEnabled = isScrimEnabled,
- )
- Shade(
- viewModel = viewModel.leftShade,
- currentTimeMillis = clock::elapsedRealtime,
- containerHeightPx = maxHeightPx,
- modifier = Modifier.align(Alignment.TopStart),
- ) {
- Column {
- StatusBar()
- Notifications()
- }
- }
- Shade(
- viewModel = viewModel.rightShade,
- currentTimeMillis = clock::elapsedRealtime,
- containerHeightPx = maxHeightPx,
- modifier = Modifier.align(Alignment.TopEnd),
- ) {
- Column {
- StatusBar()
- QuickSettings()
- }
- }
- Shade(
- viewModel = viewModel.singleShade,
- currentTimeMillis = clock::elapsedRealtime,
- containerHeightPx = maxHeightPx,
- modifier = Modifier,
- ) {
- Column {
- StatusBar()
- Notifications()
- QuickSettings()
- }
- }
- }
-}
-
-@Composable
-private fun Scrim(
- remoteTouch: (ProxiedInputModel) -> Unit,
- alpha: () -> Float,
- isScrimEnabled: Boolean,
- modifier: Modifier = Modifier,
-) {
- var size by remember { mutableStateOf(IntSize.Zero) }
-
- Box(
- modifier =
- modifier
- .graphicsLayer { this.alpha = alpha() }
- .background(colorResource(R.color.opaque_scrim))
- .fillMaxSize()
- .onSizeChanged { size = it }
- .then(
- if (isScrimEnabled) {
- Modifier.pointerInput(Unit) {
- detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) })
- }
- .pointerInput(Unit) {
- detectVerticalDragGestures(
- onVerticalDrag = { change, dragAmount ->
- remoteTouch(
- ProxiedInputModel.OnDrag(
- xFraction = change.position.x / size.width,
- yDragAmountPx = dragAmount,
- )
- )
- },
- onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) },
- onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) }
- )
- }
- } else {
- Modifier
- }
- )
- )
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
deleted file mode 100644
index cfcc2fb251fd..000000000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.composable
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.interaction.DragInteraction
-import androidx.compose.foundation.interaction.InteractionSource
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import com.android.compose.modifiers.height
-import com.android.compose.modifiers.padding
-import com.android.compose.swipeable.FixedThreshold
-import com.android.compose.swipeable.SwipeableState
-import com.android.compose.swipeable.ThresholdConfig
-import com.android.compose.swipeable.rememberSwipeableState
-import com.android.compose.swipeable.swipeable
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel
-import kotlin.math.min
-import kotlin.math.roundToInt
-import kotlinx.coroutines.launch
-
-/**
- * Renders a shade (container and content).
- *
- * This should be allowed to grow to fill the width and height of its container.
- *
- * @param viewModel The view-model for this shade.
- * @param currentTimeMillis A provider for the current time, in milliseconds.
- * @param containerHeightPx The height of the container that this shade is being shown in, in
- * pixels.
- * @param modifier The Modifier.
- * @param content The content of the shade.
- */
-@Composable
-fun Shade(
- viewModel: ShadeViewModel,
- currentTimeMillis: () -> Long,
- containerHeightPx: Float,
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit = {},
-) {
- val isVisible: Boolean by viewModel.isVisible.collectAsState()
- if (!isVisible) {
- return
- }
-
- val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
- ReportNonProxiedInput(viewModel, interactionSource)
-
- val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed)
- HandleForcedCollapse(viewModel, swipeableState)
- HandleProxiedInput(viewModel, swipeableState, currentTimeMillis)
- ReportShadeExpansion(viewModel, swipeableState, containerHeightPx)
-
- val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState()
- val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState()
- val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState()
-
- val width: ShadeViewModel.Size by viewModel.width.collectAsState()
- val density = LocalDensity.current
-
- val anchors: Map<Float, ShadeState> =
- remember(containerHeightPx) { swipeableAnchors(containerHeightPx) }
-
- ShadeContent(
- shadeHeightPx = { swipeableState.offset.value },
- overstretch = { swipeableState.overflow.value / containerHeightPx },
- isSwipingEnabled = isSwipingEnabled,
- swipeableState = swipeableState,
- interactionSource = interactionSource,
- anchors = anchors,
- thresholds = { _, to ->
- swipeableThresholds(
- to = to,
- swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx),
- swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx),
- )
- },
- modifier = modifier.shadeWidth(width, density),
- content = content,
- )
-}
-
-/**
- * Draws the content of the shade.
- *
- * @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is
- * fully collapsed.
- * @param overstretch Provider for the current amount of vertical "overstretch" that the shade
- * should be rendered with. This is `0` or a positive number that is a percentage of the total
- * height of the shade when fully expanded. A value of `0` means that the shade is not stretched
- * at all.
- * @param isSwipingEnabled Whether swiping inside the shade is enabled or not.
- * @param swipeableState The state to use for the [swipeable] modifier, allowing external control in
- * addition to direct control (proxied user input in addition to non-proxied/direct user input).
- * @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state
- * occurs; this is used to configure the [swipeable] modifier.
- * @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to
- * another. This controls how the [swipeable] decides which [ShadeState] to animate to once the
- * user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded.
- * @param content The content to render inside the shade.
- * @param modifier The [Modifier].
- */
-@Composable
-private fun ShadeContent(
- shadeHeightPx: () -> Float,
- overstretch: () -> Float,
- isSwipingEnabled: Boolean,
- swipeableState: SwipeableState<ShadeState>,
- interactionSource: MutableInteractionSource,
- anchors: Map<Float, ShadeState>,
- thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig,
- modifier: Modifier = Modifier,
- content: @Composable () -> Unit = {},
-) {
- /**
- * Returns a function that takes in [Density] and returns the current padding around the shade
- * content.
- */
- fun padding(
- shadeHeightPx: () -> Float,
- ): Density.() -> Int {
- return {
- min(
- 12.dp.toPx().roundToInt(),
- shadeHeightPx().roundToInt(),
- )
- }
- }
-
- Surface(
- shape = RoundedCornerShape(32.dp),
- modifier =
- modifier
- .fillMaxWidth()
- .height { shadeHeightPx().roundToInt() }
- .padding(
- horizontal = padding(shadeHeightPx),
- vertical = padding(shadeHeightPx),
- )
- .graphicsLayer {
- // Applies the vertical over-stretching of the shade content that may happen if
- // the user keep dragging down when the shade is already fully-expanded.
- transformOrigin = transformOrigin.copy(pivotFractionY = 0f)
- this.scaleY = 1 + overstretch().coerceAtLeast(0f)
- }
- .swipeable(
- enabled = isSwipingEnabled,
- state = swipeableState,
- interactionSource = interactionSource,
- anchors = anchors,
- thresholds = thresholds,
- orientation = Orientation.Vertical,
- ),
- content = content,
- )
-}
-
-/** Funnels current shade expansion values into the view-model. */
-@Composable
-private fun ReportShadeExpansion(
- viewModel: ShadeViewModel,
- swipeableState: SwipeableState<ShadeState>,
- containerHeightPx: Float,
-) {
- LaunchedEffect(swipeableState.offset, containerHeightPx) {
- snapshotFlow { swipeableState.offset.value / containerHeightPx }
- .collect { expansion -> viewModel.onExpansionChanged(expansion) }
- }
-}
-
-/** Funnels drag gesture start and end events into the view-model. */
-@Composable
-private fun ReportNonProxiedInput(
- viewModel: ShadeViewModel,
- interactionSource: InteractionSource,
-) {
- LaunchedEffect(interactionSource) {
- interactionSource.interactions.collect {
- when (it) {
- is DragInteraction.Start -> {
- viewModel.onDragStarted()
- }
- is DragInteraction.Stop -> {
- viewModel.onDragEnded()
- }
- }
- }
- }
-}
-
-/** When told to force collapse, collapses the shade. */
-@Composable
-private fun HandleForcedCollapse(
- viewModel: ShadeViewModel,
- swipeableState: SwipeableState<ShadeState>,
-) {
- LaunchedEffect(viewModel) {
- viewModel.isForceCollapsed.collect {
- launch { swipeableState.animateTo(ShadeState.FullyCollapsed) }
- }
- }
-}
-
-/**
- * Handles proxied input (input originating outside of the UI of the shade) by driving the
- * [SwipeableState] accordingly.
- */
-@Composable
-private fun HandleProxiedInput(
- viewModel: ShadeViewModel,
- swipeableState: SwipeableState<ShadeState>,
- currentTimeMillis: () -> Long,
-) {
- val velocityTracker: VelocityTracker = remember { VelocityTracker() }
- LaunchedEffect(viewModel) {
- viewModel.proxiedInput.collect {
- when (it) {
- is ProxiedInputModel.OnDrag -> {
- velocityTracker.addPosition(
- timeMillis = currentTimeMillis.invoke(),
- position = Offset(0f, it.yDragAmountPx),
- )
- swipeableState.performDrag(it.yDragAmountPx)
- }
- is ProxiedInputModel.OnDragEnd -> {
- launch {
- val velocity = velocityTracker.calculateVelocity().y
- velocityTracker.resetTracking()
- // We use a VelocityTracker to keep a record of how fast the pointer was
- // moving such that we know how far to fling the shade when the gesture
- // ends. Flinging the SwipeableState using performFling is required after
- // one or more calls to performDrag such that the swipeable settles into one
- // of the states. Without doing that, the shade would remain unmoving in an
- // in-between state on the screen.
- swipeableState.performFling(velocity)
- }
- }
- is ProxiedInputModel.OnDragCancel -> {
- launch {
- velocityTracker.resetTracking()
- swipeableState.animateTo(swipeableState.progress.from)
- }
- }
- else -> Unit
- }
- }
- }
-}
-
-/**
- * Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp.
- *
- * @param density The [Density] of the display.
- * @param wholePx The whole amount that the given [Float] is a fraction of.
- * @return The dp size that's a fraction of the whole amount.
- */
-private fun Float.fractionToDp(density: Density, wholePx: Float): Dp {
- return with(density) { (this@fractionToDp * wholePx).toDp() }
-}
-
-private fun Modifier.shadeWidth(
- size: ShadeViewModel.Size,
- density: Density,
-): Modifier {
- return then(
- when (size) {
- is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction)
- is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() })
- }
- )
-}
-
-/** Returns the pixel positions for each of the supported shade states. */
-private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> {
- return mapOf(
- 0f to ShadeState.FullyCollapsed,
- containerHeightPx to ShadeState.FullyExpanded,
- )
-}
-
-/**
- * Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it
- * actually completes the expansion or collapse after the user lifts their pointer.
- */
-private fun swipeableThresholds(
- to: ShadeState,
- swipeExpandThreshold: Dp,
- swipeCollapseThreshold: Dp,
-): ThresholdConfig {
- return FixedThreshold(
- when (to) {
- ShadeState.FullyExpanded -> swipeExpandThreshold
- ShadeState.FullyCollapsed -> swipeCollapseThreshold
- }
- )
-}
-
-/** Enumerates the shade UI states for [SwipeableState]. */
-private enum class ShadeState {
- FullyCollapsed,
- FullyExpanded,
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 5e0761063af2..32986649388d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -92,6 +92,14 @@ private fun Scene(
onSceneChanged: (SceneModel) -> Unit,
modifier: Modifier = Modifier,
) {
+ val destinationScenes: Map<UserAction, SceneModel> by
+ scene.destinationScenes(containerName).collectAsState()
+ val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
+ val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
+ val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
+ val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
+ val backDestinationScene = destinationScenes[UserAction.Back]
+
// TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress.
Box(modifier) {
Column(
@@ -103,14 +111,6 @@ private fun Scene(
modifier = Modifier,
)
- val destinationScenes: Map<UserAction, SceneModel> by
- scene.destinationScenes(containerName).collectAsState()
- val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)]
- val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)]
- val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)]
- val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)]
- val backDestinationScene = destinationScenes[UserAction.Back]
-
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
index 95a9ce960dcd..d43276c00f87 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt
@@ -187,6 +187,9 @@ object CustomizationProviderContract {
/** Flag denoting transit clock are enabled in wallpaper picker. */
const val FLAG_NAME_TRANSIT_CLOCK = "lockscreen_custom_transit_clock"
+ /** Flag denoting transit clock are enabled in wallpaper picker. */
+ const val FLAG_NAME_PAGE_TRANSITIONS = "wallpaper_picker_page_transitions"
+
object Columns {
/** String. Unique ID for the flag. */
const val NAME = "name"
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 18753fd9c0c7..006fc09fb400 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -422,9 +422,6 @@
-packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallFlags.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLogger.kt
-packages/SystemUI/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherContainer.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/ConnectivitySlots.kt
--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
-packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryStateNotifier.kt
-packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt
-packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -696,7 +693,6 @@
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallChronometerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryStateNotifierTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
-packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
index 57b3acd6557a..66c54f2a668e 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml
@@ -21,6 +21,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyguard_lock_padding"
+ android:importantForAccessibility="no"
android:ellipsize="marquee"
android:focusable="true"
android:gravity="center"
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
new file mode 100644
index 000000000000..40bab5ed08f2
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorPrimary"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorPrimary"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_large_background.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_large_background.xml
new file mode 100644
index 000000000000..f8d4af57229b
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_large_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorPrimaryFixed"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorPrimaryFixed"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_large_text.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_large_text.xml
new file mode 100644
index 000000000000..faba8fc4c755
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_large_text.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorOnPrimaryFixed"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorOnPrimaryFixed"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml
new file mode 100644
index 000000000000..e76ad991a92c
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorOnPrimary"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorOnPrimary"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_outline.xml b/packages/SystemUI/res/color/qs_dialog_btn_outline.xml
new file mode 100644
index 000000000000..1adfe5b19d70
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_outline.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorPrimary"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorPrimary"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_dialog_btn_outline_text.xml b/packages/SystemUI/res/color/qs_dialog_btn_outline_text.xml
new file mode 100644
index 000000000000..5dc994f23f2b
--- /dev/null
+++ b/packages/SystemUI/res/color/qs_dialog_btn_outline_text.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:state_enabled="false"
+ android:color="?androidprv:attr/materialColorOnSurface"
+ android:alpha="0.30"/>
+ <item android:color="?androidprv:attr/materialColorOnSurface"/>
+</selector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml
new file mode 100644
index 000000000000..16076b17a6e5
--- /dev/null
+++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml
new file mode 100644
index 000000000000..309770ddd76d
--- /dev/null
+++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
index 54bdf18e3076..bc1775ee64ae 100644
--- a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
+++ b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
@@ -13,30 +14,22 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="56dp"
- android:height="24dp"
- android:viewportWidth="56"
- android:viewportHeight="24">
- <path
- android:pathData="M12,0L44,0A12,12 0,0 1,56 12L56,12A12,12 0,0 1,44 24L12,24A12,12 0,0 1,0 12L0,12A12,12 0,0 1,12 0z"
- android:fillColor="#ffffff"/>
- <group
- android:scaleX="0.8"
- android:scaleY="0.8"
- android:translateY="2"
- android:translateX="18">
- <path
- android:pathData="M21.5,9C22.3284,9 23,8.3284 23,7.5C23,6.6716 22.3284,6 21.5,6C20.6716,6 20,6.6716 20,7.5C20,8.3284 20.6716,9 21.5,9Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M17,14C18.6569,14 20,12.6569 20,11C20,9.3432 18.6569,8 17,8C15.3431,8 14,9.3432 14,11C14,12.6569 15.3431,14 17,14Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M17,22C18.933,22 20.5,20.433 20.5,18.5C20.5,16.567 18.933,15 17,15C15.067,15 13.5,16.567 13.5,18.5C13.5,20.433 15.067,22 17,22Z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M7,14C10.3137,14 13,11.3137 13,8C13,4.6863 10.3137,2 7,2C3.6863,2 1,4.6863 1,8C1,11.3137 3.6863,14 7,14Z"
- android:fillColor="#000000"/>
- </group>
-</vector> \ No newline at end of file
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@id/background"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <size
+ android:height="24px"
+ android:width="24px"
+ />
+ <solid android:color="#FFFFFFFF" />
+ </shape>
+ </item>
+ <item android:id="@id/icon"
+ android:gravity="center"
+ android:width="20px"
+ android:height="20px"
+ android:drawable="@drawable/ic_person_outline"
+ />
+</layer-list> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_person_outline.xml b/packages/SystemUI/res/drawable/ic_person_outline.xml
new file mode 100644
index 000000000000..d94714e0d51a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_person_outline.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/black"
+ android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720L480,720Z"/>
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
index 4029702ec6b4..32e88ab22b91 100644
--- a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
+++ b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
- <solid android:color="@android:color/white" />
+ <solid android:color="?android:attr/colorBackground" />
<size
android:height="56dp"
diff --git a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
index e3c7d0ce89aa..12c3e23bf0a0 100644
--- a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
+++ b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml
@@ -17,7 +17,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
- <solid android:color="#80ffffff" />
+ <solid android:color="?android:attr/colorBackground" />
<size
android:height="76dp"
diff --git a/packages/SystemUI/res/drawable/media_output_icon_volume.xml b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
index fce4e0022c7a..85d608fa736f 100644
--- a/packages/SystemUI/res/drawable/media_output_icon_volume.xml
+++ b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
@@ -3,7 +3,8 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
- android:tint="?attr/colorControlNormal">
+ android:tint="?attr/colorControlNormal"
+ android:autoMirrored="true">
<path
android:fillColor="@color/media_dialog_item_main_content"
android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.125,8.475 15.812,9.575Q16.5,10.675 16.5,12Q16.5,13.325 15.812,14.4Q15.125,15.475 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/>
diff --git a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
index b93793773179..a8779002c1b3 100644
--- a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
+++ b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml
@@ -17,9 +17,9 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
- android:bottomLeftRadius="28dp"
- android:topLeftRadius="28dp"
- android:bottomRightRadius="0dp"
- android:topRightRadius="0dp"/>
+ android:bottomLeftRadius="@dimen/media_output_dialog_icon_left_radius"
+ android:topLeftRadius="@dimen/media_output_dialog_icon_left_radius"
+ android:bottomRightRadius="@dimen/media_output_dialog_icon_right_radius"
+ android:topRightRadius="@dimen/media_output_dialog_icon_right_radius"/>
<solid android:color="@color/media_dialog_item_background" />
</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml
new file mode 100644
index 000000000000..f63c2ffbfdad
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/background">
+ <shape
+ android:shape="oval"
+ android:id="@id/background"
+ android:gravity="center">
+ <size
+ android:height="24dp"
+ android:width="24dp"/>
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml
new file mode 100644
index 000000000000..5d5529ff1a50
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml
new file mode 100644
index 000000000000..310b0becf10c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_large" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml
new file mode 100644
index 000000000000..e89bdd31ca17
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml
new file mode 100644
index 000000000000..fcf0b1c5091f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topLeftRadius="@dimen/privacy_dialog_background_radius_small"
+ android:topRightRadius="@dimen/privacy_dialog_background_radius_small" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
new file mode 100644
index 000000000000..b9f5d60abd8b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9.55,18l-5.7,-5.7 1.425,-1.425L9.55,15.15l9.175,-9.175L20.15,7.4z"/>
+</vector> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml
new file mode 100644
index 000000000000..ea8f9c2839ec
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24.0dp"
+ android:height="24.0dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M26.0,14.0l-4.0,0.0l0.0,4.0l4.0,0.0l0.0,-4.0zm0.0,8.0l-4.0,0.0l0.0,12.0l4.0,0.0L26.0,22.0zm8.0,-19.98L14.0,2.0c-2.21,0.0 -4.0,1.79 -4.0,4.0l0.0,36.0c0.0,2.21 1.79,4.0 4.0,4.0l20.0,0.0c2.21,0.0 4.0,-1.79 4.0,-4.0L38.0,6.0c0.0,-2.21 -1.79,-3.98 -4.0,-3.98zM34.0,38.0L14.0,38.0L14.0,10.0l20.0,0.0l0.0,28.0z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml
new file mode 100644
index 000000000000..f8b99f4a0ee4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml
new file mode 100644
index 000000000000..ae60d517ceb4
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z"
+ android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/>
+ <path
+ android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527"
+ android:fillColor="?androidprv:attr/materialColorOnSurface"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
index c4e45bf2c223..9bc8b53b308e 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
@@ -15,7 +15,6 @@
~ limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:insetTop="@dimen/dialog_button_vertical_inset"
android:insetBottom="@dimen/dialog_button_vertical_inset">
<ripple android:color="?android:attr/colorControlHighlight">
@@ -28,7 +27,7 @@
<item>
<shape android:shape="rectangle">
<corners android:radius="?android:attr/buttonCornerRadius"/>
- <solid android:color="?androidprv:attr/materialColorPrimary"/>
+ <solid android:color="@color/qs_dialog_btn_filled_background"/>
<padding android:left="@dimen/dialog_button_horizontal_padding"
android:top="@dimen/dialog_button_vertical_padding"
android:right="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
index 1590daa8b7f9..50405cab3b95 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled_large.xml
@@ -26,7 +26,7 @@
<item>
<shape android:shape="rectangle">
<corners android:radius="18dp"/>
- <solid android:color="?androidprv:attr/materialColorPrimaryFixed"/>
+ <solid android:color="@color/qs_dialog_btn_filled_large_background"/>
</shape>
</item>
</ripple>
diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
index b0dc6523e971..9e9533aa2052 100644
--- a/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
+++ b/packages/SystemUI/res/drawable/qs_dialog_btn_outline.xml
@@ -15,7 +15,6 @@
~ limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:insetTop="@dimen/dialog_button_vertical_inset"
android:insetBottom="@dimen/dialog_button_vertical_inset">
<ripple android:color="?android:attr/colorControlHighlight">
@@ -29,7 +28,7 @@
<shape android:shape="rectangle">
<corners android:radius="?android:attr/buttonCornerRadius"/>
<solid android:color="@android:color/transparent"/>
- <stroke android:color="?androidprv:attr/materialColorPrimary"
+ <stroke android:color="@color/qs_dialog_btn_outline"
android:width="1dp"
/>
<padding android:left="@dimen/dialog_button_horizontal_padding"
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index bb32022a0b5f..82410703c9e6 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -110,7 +110,7 @@
<ImageView
android:id="@+id/dream_overlay_assistant_attention_indicator"
- android:layout_width="@dimen/dream_overlay_grey_chip_width"
+ android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
android:src="@drawable/dream_overlay_assistant_attention_indicator"
diff --git a/packages/SystemUI/res/layout/immersive_mode_cling.xml b/packages/SystemUI/res/layout/immersive_mode_cling.xml
index bfb8184ee044..e6529b9aa9a1 100644
--- a/packages/SystemUI/res/layout/immersive_mode_cling.xml
+++ b/packages/SystemUI/res/layout/immersive_mode_cling.xml
@@ -58,7 +58,7 @@
android:paddingStart="48dp"
android:paddingTop="40dp"
android:text="@string/immersive_cling_title"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="24sp" />
<TextView
@@ -70,7 +70,7 @@
android:paddingStart="48dp"
android:paddingTop="12.6dp"
android:text="@string/immersive_cling_description"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="16sp" />
<Button
@@ -85,7 +85,7 @@
android:paddingEnd="8dp"
android:paddingStart="8dp"
android:text="@string/immersive_cling_positive"
- android:textColor="@android:color/white"
+ android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="14sp" />
</RelativeLayout>
diff --git a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
deleted file mode 100644
index 9304ff72f054..000000000000
--- a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml
+++ /dev/null
@@ -1,136 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2019 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-
-<!-- Layout for media recommendations inside QSPanel carousel -->
-<!-- See media_recommendation_expanded.xml and media_recommendation_collapsed.xml for the
- constraints. -->
-<com.android.systemui.util.animation.TransitionLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/media_recommendations"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:forceHasOverlappingRendering="false"
- android:background="@drawable/qs_media_background"
- android:theme="@style/MediaPlayer">
-
- <!-- This view just ensures the full media player is a certain height. -->
- <View
- android:id="@+id/sizing_view"
- android:layout_width="match_parent"
- android:layout_height="@dimen/qs_media_session_height_expanded" />
-
- <com.android.internal.widget.CachingIconView
- android:id="@+id/recommendation_card_icon"
- android:layout_width="@dimen/qs_media_app_icon_size"
- android:layout_height="@dimen/qs_media_app_icon_size"
- android:minWidth="@dimen/qs_media_app_icon_size"
- android:minHeight="@dimen/qs_media_app_icon_size"
- android:layout_marginStart="@dimen/qs_media_padding"
- android:layout_marginTop="@dimen/qs_media_rec_icon_top_margin"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <FrameLayout
- android:id="@+id/media_cover1_container"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- >
- <ImageView
- android:id="@+id/media_cover1"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:minWidth="@dimen/qs_media_rec_album_size"
- android:minHeight="@dimen/qs_media_rec_album_size"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- android:adjustViewBounds="true"
- android:background="@drawable/bg_smartspace_media_item"
- style="@style/MediaPlayer.Recommendation.Album"
- android:clipToOutline="true"
- android:scaleType="centerCrop"/>
- </FrameLayout>
-
- <TextView
- android:id="@+id/media_title1"
- style="@style/MediaPlayer.Recommendation.Text.Title"
- />
-
- <TextView
- android:id="@+id/media_subtitle1"
- style="@style/MediaPlayer.Recommendation.Text.Subtitle"
- />
-
- <FrameLayout
- android:id="@+id/media_cover2_container"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- >
- <ImageView
- android:id="@+id/media_cover2"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:minWidth="@dimen/qs_media_rec_album_size"
- android:minHeight="@dimen/qs_media_rec_album_size"
- android:adjustViewBounds="true"
- android:background="@drawable/bg_smartspace_media_item"
- style="@style/MediaPlayer.Recommendation.Album"
- android:clipToOutline="true"
- android:scaleType="centerCrop"/>
- </FrameLayout>
-
- <TextView
- android:id="@+id/media_title2"
- style="@style/MediaPlayer.Recommendation.Text.Title"
- />
-
- <TextView
- android:id="@+id/media_subtitle2"
- style="@style/MediaPlayer.Recommendation.Text.Subtitle"
- />
-
- <FrameLayout
- android:id="@+id/media_cover3_container"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- >
- <ImageView
- android:id="@+id/media_cover3"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:minWidth="@dimen/qs_media_rec_album_size"
- android:minHeight="@dimen/qs_media_rec_album_size"
- android:adjustViewBounds="true"
- android:background="@drawable/bg_smartspace_media_item"
- style="@style/MediaPlayer.Recommendation.Album"
- android:clipToOutline="true"
- android:scaleType="centerCrop"/>
- </FrameLayout>
-
- <TextView
- android:id="@+id/media_title3"
- style="@style/MediaPlayer.Recommendation.Text.Title"
- />
-
- <TextView
- android:id="@+id/media_subtitle3"
- style="@style/MediaPlayer.Recommendation.Text.Subtitle"
- />
-
- <include
- layout="@layout/media_long_press_menu" />
-
-</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/layout/privacy_dialog_card_button.xml b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml
new file mode 100644
index 000000000000..e297b939e2b8
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_marginBottom="4dp"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"
+ style="@style/Widget.Dialog.Button.BorderButton"/> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
new file mode 100644
index 000000000000..b84f3a9794be
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml
@@ -0,0 +1,89 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/privacy_dialog_item_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:foreground="?android:attr/selectableItemBackground"
+ app:cardCornerRadius="28dp"
+ app:cardElevation="0dp"
+ app:cardBackgroundColor="?androidprv:attr/materialColorSurfaceBright">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/privacy_dialog_item_header"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingBottom="20dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp">
+ <ImageView
+ android:id="@+id/privacy_dialog_item_header_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:importantForAccessibility="no" />
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="match_parent"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:layout_centerVertical="true">
+ <TextView
+ android:id="@+id/privacy_dialog_item_header_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:hyphenationFrequency="normalFast"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Title" />
+ <TextView
+ android:id="@+id/privacy_dialog_item_header_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Summary" />
+ </LinearLayout>
+ <ImageView
+ android:id="@+id/privacy_dialog_item_header_expand_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:visibility="gone" />
+ </LinearLayout>
+ <LinearLayout
+ android:id="@+id/privacy_dialog_item_header_expanded_layout"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:visibility="gone">
+ </LinearLayout>
+ </LinearLayout>
+</androidx.cardview.widget.CardView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/privacy_dialog_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_v2.xml
new file mode 100644
index 000000000000..843dad03bca4
--- /dev/null
+++ b/packages/SystemUI/res/layout/privacy_dialog_v2.xml
@@ -0,0 +1,109 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.core.widget.NestedScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="@dimen/large_dialog_width"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:orientation="vertical">
+
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:fontFamily="@*android:string/config_headlineFontFamily"
+ android:text="@string/privacy_dialog_title"
+ android:layout_marginBottom="12dp"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_summary"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ android:gravity="center"
+ android:layout_marginBottom="20dp"/>
+ </LinearLayout>
+
+ <!-- Items -->
+ <LinearLayout
+ android:id="@+id/privacy_dialog_items_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:orientation="vertical"
+ />
+
+ <!-- Buttons -->
+ <LinearLayout
+ android:id="@+id/button_layout"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="18dp"
+ android:clickable="false"
+ android:focusable="false">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="start|center_vertical"
+ android:orientation="vertical">
+ <Button
+ android:id="@+id/privacy_dialog_more_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_more_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="end|center_vertical">
+ <Button
+ android:id="@+id/privacy_dialog_close_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/privacy_dialog_done_button"
+ android:ellipsize="end"
+ android:maxLines="1"
+ style="@style/Widget.Dialog.Button.BorderButton"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="center"/>
+ </LinearLayout>
+ </LinearLayout>
+ </LinearLayout>
+</androidx.core.widget.NestedScrollView> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/qs_tile_side_icon.xml b/packages/SystemUI/res/layout/qs_tile_side_icon.xml
index f1b7259f4c1d..fbcead1cbb8b 100644
--- a/packages/SystemUI/res/layout/qs_tile_side_icon.xml
+++ b/packages/SystemUI/res/layout/qs_tile_side_icon.xml
@@ -30,12 +30,11 @@
android:visibility="gone"
/>
- <ImageView
+ <com.android.systemui.qs.tileimpl.ChevronImageView
android:id="@+id/chevron"
android:layout_width="@dimen/qs_icon_size"
android:layout_height="@dimen/qs_icon_size"
android:src="@*android:drawable/ic_chevron_end"
- android:autoMirrored="true"
android:visibility="gone"
android:importantForAccessibility="no"
/>
diff --git a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
deleted file mode 100644
index d6c63eb4feac..000000000000
--- a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 2018, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-<com.android.systemui.statusbar.StatusBarMobileView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/mobile_combo"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="center_vertical" >
-
- <include layout="@layout/status_bar_mobile_signal_group_inner" />
-
-</com.android.systemui.statusbar.StatusBarMobileView>
-
diff --git a/packages/SystemUI/res/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml
new file mode 100644
index 000000000000..0d99b617819b
--- /dev/null
+++ b/packages/SystemUI/res/values-ldrtl/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <dimen name="media_output_dialog_icon_left_radius">0dp</dimen>
+ <dimen name="media_output_dialog_icon_right_radius">28dp</dimen>
+</resources> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index db7eb7a049e7..ab754985e11d 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -16,7 +16,8 @@
* limitations under the License.
*/
-->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<drawable name="notification_number_text_color">#ffffffff</drawable>
<drawable name="system_bar_background">@color/system_bar_background_opaque</drawable>
<color name="system_bar_background_opaque">#ff000000</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3366f4f6d443..5a15dcec5223 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1097,6 +1097,9 @@
<dimen name="ongoing_appops_dialog_side_padding">16dp</dimen>
+ <dimen name="privacy_dialog_background_radius_large">12dp</dimen>
+ <dimen name="privacy_dialog_background_radius_small">4dp</dimen>
+
<!-- Size of media cards in the QSPanel carousel -->
<dimen name="qs_media_padding">16dp</dimen>
<dimen name="qs_media_album_radius">14dp</dimen>
@@ -1346,6 +1349,8 @@
<dimen name="media_output_dialog_default_margin_end">16dp</dimen>
<dimen name="media_output_dialog_selectable_margin_end">80dp</dimen>
<dimen name="media_output_dialog_list_padding_top">8dp</dimen>
+ <dimen name="media_output_dialog_icon_left_radius">28dp</dimen>
+ <dimen name="media_output_dialog_icon_right_radius">0dp</dimen>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
like "expand". -->
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 3a2177a0045c..15ca9d48c62a 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -214,4 +214,8 @@
<item type="id" name="nssl_guideline" />
<item type="id" name="lock_icon" />
<item type="id" name="lock_icon_bg" />
+
+ <!-- Privacy dialog -->
+ <item type="id" name="privacy_dialog_close_app_button" />
+ <item type="id" name="privacy_dialog_manage_app_button" />
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f8c13b008fd9..983b09f957ae 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -369,6 +369,8 @@
<string name="biometric_dialog_tap_confirm_with_face_alt_3">Face recognized. Press the unlock icon to continue.</string>
<!-- Talkback string when a biometric is authenticated [CHAR LIMIT=NONE] -->
<string name="biometric_dialog_authenticated">Authenticated</string>
+ <!-- Talkback string when a canceling authentication [CHAR LIMIT=NONE] -->
+ <string name="biometric_dialog_cancel_authentication">Cancel Authentication</string>
<!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] -->
<string name="biometric_dialog_use_pin">Use PIN</string>
@@ -953,6 +955,8 @@
<!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
<string name="keyguard_retry">Swipe up to try again</string>
+ <!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
+ <string name="accesssibility_keyguard_retry">Swipe up to try Face Unlock again</string>
<!-- Message shown when notifying user to unlock in order to use NFC. [CHAR LIMIT=60] -->
<string name="require_unlock_for_nfc">Unlock to use NFC</string>
@@ -2582,6 +2586,9 @@
<!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] -->
<string name="controls_structure_tooltip">Swipe to see more</string>
+ <!-- Accessibility action informing the user how they can retry face authentication [CHAR LIMIT=NONE] -->
+ <string name="retry_face">Retry face authentication</string>
+
<!-- Message to tell the user to wait while systemui attempts to load a set of
recommended controls [CHAR_LIMIT=60] -->
<string name="controls_seeding_in_progress">Loading recommendations</string>
@@ -3167,10 +3174,43 @@
<!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] -->
<string name="set_default_notes_app_toast_content">Set default notes app in Settings</string>
- <!--
- Label for a button that, when clicked, sends the user to the app store to install an app.
-
- [CHAR LIMIT=64].
- -->
+ <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. -->
<string name="install_app">Install app</string>
+
+ <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] -->
+ <string name="privacy_dialog_title">Microphone &amp; Camera</string>
+ <!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_summary">Recent app use</string>
+ <!-- Label of the secondary button of the privacy dialog, used to check recent app usage of phone sensors [CHAR LIMIT=30] -->
+ <string name="privacy_dialog_more_button">See recent access</string>
+ <!-- Label of the primary button to dismiss the privacy dialog [CHAR LIMIT=20] -->
+ <string name="privacy_dialog_done_button">Done</string>
+ <!-- Description for expanding a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_expand_action">Expand and show options</string>
+ <!-- Description for collapsing a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_collapse_action">Collapse</string>
+ <!-- Label of a button of the privacy dialog to close an app actively using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_close_app_button">Close this app</string>
+ <!-- Message shown in the privacy dialog when an app actively using a phone sensor is closed [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_close_app_message"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> closed</string>
+ <!-- Label of a button of the privacy dialog to learn more of a service actively or recently using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_manage_service">Manage service</string>
+ <!-- Label of a button of the privacy dialog to manage permissions of an app actively or recently using a phone sensor [CHAR LIMIT=50] -->
+ <string name="privacy_dialog_manage_permissions">Manage access</string>
+ <!-- Label for active usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_call_usage">In use by phone call</string>
+ <!-- Label for recent usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_call_usage">Recently used in phone call</string>
+ <!-- Label for active app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
+ <!-- Label for recent app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string>
+ <!-- Label for active app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage_1">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
+ <!-- Label for recent app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage_1">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string>
+ <!-- Label for active app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+ <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
+ <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
</resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6b8562105c74..d520670ec012 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1116,7 +1116,7 @@
<style name="Widget.Dialog.Button">
<item name="android:buttonCornerRadius">28dp</item>
<item name="android:background">@drawable/qs_dialog_btn_filled</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item>
+ <item name="android:textColor">@color/qs_dialog_btn_filled_text_color</item>
<item name="android:textSize">14sp</item>
<item name="android:lineHeight">20sp</item>
<item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
@@ -1126,13 +1126,13 @@
<style name="Widget.Dialog.Button.BorderButton">
<item name="android:background">@drawable/qs_dialog_btn_outline</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ <item name="android:textColor">@color/qs_dialog_btn_outline_text</item>
</style>
<style name="Widget.Dialog.Button.Large">
<item name="android:background">@drawable/qs_dialog_btn_filled_large</item>
<item name="android:minHeight">56dp</item>
- <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryFixed</item>
+ <item name="android:textColor">@color/qs_dialog_btn_filled_large_text</item>
</style>
<style name="Widget.Dialog.Button.QuickSettings">
@@ -1439,4 +1439,22 @@
<item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item>
<item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item>
</style>
+
+ <style name="TextAppearance.PrivacyDialog.Item.Title"
+ parent="@android:style/TextAppearance.DeviceDefault.Medium">
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ </style>
+
+ <style name="TextAppearance.PrivacyDialog.Item.Summary"
+ parent="@android:style/TextAppearance.DeviceDefault.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+ </style>
+
+ <style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog">
+ <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml b/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
deleted file mode 100644
index b7d4b3aac079..000000000000
--- a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-<ConstraintSet
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto" >
-
- <Constraint
- android:id="@+id/sizing_view"
- android:layout_width="match_parent"
- android:layout_height="@dimen/qs_media_session_height_collapsed"
- />
-
- <!-- Only the constraintBottom and marginBottom are different. The rest of the constraints are
- the same as the constraints in media_recommendations_expanded.xml. But, due to how
- ConstraintSets work, all the constraints need to be in the same place. So, the shared
- constraints can't be put in the shared layout file media_smartspace_recommendations.xml and
- the constraints are instead duplicated between here and media_recommendations_expanded.xml.
- Ditto for the other cover containers. -->
- <Constraint
- android:id="@+id/media_cover1_container"
- app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginBottom="@dimen/qs_media_padding"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/media_cover2_container"
- android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintHorizontal_bias="1.0"
- app:layout_constraintVertical_bias="0.5"
- />
-
- <Constraint
- android:id="@+id/media_title1"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/media_subtitle1"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/media_cover2_container"
- app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginBottom="@dimen/qs_media_padding"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toEndOf="@id/media_cover1_container"
- app:layout_constraintEnd_toStartOf="@id/media_cover3_container"
- android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
- app:layout_constraintVertical_bias="0.5"
- />
-
- <Constraint
- android:id="@+id/media_title2"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/media_subtitle2"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/media_cover3_container"
- app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginBottom="@dimen/qs_media_padding"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toEndOf="@id/media_cover2_container"
- app:layout_constraintEnd_toEndOf="parent"
- android:layout_marginEnd="@dimen/qs_media_padding"
- app:layout_constraintVertical_bias="0.5"
- />
-
- <Constraint
- android:id="@+id/media_title3"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/media_subtitle3"
- android:visibility="gone"
- />
-
-</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_recommendation_expanded.xml b/packages/SystemUI/res/xml/media_recommendation_expanded.xml
deleted file mode 100644
index ce25a7d01bf7..000000000000
--- a/packages/SystemUI/res/xml/media_recommendation_expanded.xml
+++ /dev/null
@@ -1,123 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
- -->
-<ConstraintSet
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- >
-
- <Constraint
- android:id="@+id/sizing_view"
- android:layout_width="match_parent"
- android:layout_height="@dimen/qs_media_session_height_expanded"
- />
-
- <Constraint
- android:id="@+id/media_cover1_container"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@+id/media_title1"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/media_cover2_container"
- android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintVertical_chainStyle="packed"
- app:layout_constraintHorizontal_bias="1.0"
- app:layout_constraintVertical_bias="0.4"
- />
-
- <Constraint
- android:id="@+id/media_title1"
- style="@style/MediaPlayer.Recommendation.Text.Title"
- app:layout_constraintStart_toStartOf="@+id/media_cover1_container"
- app:layout_constraintEnd_toEndOf="@+id/media_cover1_container"
- app:layout_constraintTop_toBottomOf="@+id/media_cover1_container"
- app:layout_constraintBottom_toTopOf="@+id/media_subtitle1"
- />
-
- <Constraint
- android:id="@+id/media_subtitle1"
- style="@style/MediaPlayer.Recommendation.Text.Subtitle"
- app:layout_constraintStart_toStartOf="@+id/media_cover1_container"
- app:layout_constraintEnd_toEndOf="@+id/media_cover1_container"
- app:layout_constraintTop_toBottomOf="@+id/media_title1"
- app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginBottom="@dimen/qs_media_padding"
- />
-
- <Constraint
- android:id="@+id/media_cover2_container"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@id/media_title2"
- app:layout_constraintStart_toEndOf="@id/media_cover1_container"
- app:layout_constraintEnd_toStartOf="@id/media_cover3_container"
- android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin"
- app:layout_constraintVertical_chainStyle="packed"
- app:layout_constraintVertical_bias="0.4"
- />
-
- <Constraint
- android:id="@+id/media_title2"
- style="@style/MediaPlayer.Recommendation.Text.Title"
- app:layout_constraintStart_toStartOf="@+id/media_cover2_container"
- app:layout_constraintEnd_toEndOf="@+id/media_cover2_container"
- app:layout_constraintTop_toBottomOf="@+id/media_cover2_container"
- app:layout_constraintBottom_toTopOf="@+id/media_subtitle2"
- />
-
- <Constraint
- android:id="@+id/media_subtitle2"
- style="@style/MediaPlayer.Recommendation.Text.Subtitle"
- app:layout_constraintStart_toStartOf="@+id/media_cover2_container"
- app:layout_constraintEnd_toEndOf="@+id/media_cover2_container"
- app:layout_constraintTop_toBottomOf="@+id/media_title2"
- app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginBottom="@dimen/qs_media_padding"
- />
-
- <Constraint
- android:id="@+id/media_cover3_container"
- style="@style/MediaPlayer.Recommendation.AlbumContainer"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@id/media_title3"
- app:layout_constraintStart_toEndOf="@id/media_cover2_container"
- app:layout_constraintEnd_toEndOf="parent"
- android:layout_marginEnd="@dimen/qs_media_padding"
- app:layout_constraintVertical_chainStyle="packed"
- app:layout_constraintVertical_bias="0.4"
- />
-
- <Constraint
- android:id="@+id/media_title3"
- style="@style/MediaPlayer.Recommendation.Text.Title"
- app:layout_constraintStart_toStartOf="@+id/media_cover3_container"
- app:layout_constraintEnd_toEndOf="@+id/media_cover3_container"
- app:layout_constraintTop_toBottomOf="@+id/media_cover3_container"
- app:layout_constraintBottom_toTopOf="@+id/media_subtitle3"
- />
-
- <Constraint
- android:id="@+id/media_subtitle3"
- style="@style/MediaPlayer.Recommendation.Text.Subtitle"
- app:layout_constraintStart_toStartOf="@+id/media_cover3_container"
- app:layout_constraintEnd_toEndOf="@+id/media_cover3_container"
- app:layout_constraintTop_toBottomOf="@+id/media_title3"
- app:layout_constraintBottom_toBottomOf="parent"
- android:layout_marginBottom="@dimen/qs_media_padding"
- />
-
-</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml b/packages/SystemUI/res/xml/media_recommendations_collapsed.xml
index d3be3c7de5ad..d3be3c7de5ad 100644
--- a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_recommendations_collapsed.xml
diff --git a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml b/packages/SystemUI/res/xml/media_recommendations_expanded.xml
index 88c70552e9e8..88c70552e9e8 100644
--- a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml
+++ b/packages/SystemUI/res/xml/media_recommendations_expanded.xml
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index d8085b9f9f2e..22cdb30376d0 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -20,6 +20,7 @@ import android.annotation.StringDef
import android.os.PowerManager
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.keyguard.FaceAuthApiRequestReason.Companion.ACCESSIBILITY_ACTION
import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED
import com.android.keyguard.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED
import com.android.keyguard.FaceAuthApiRequestReason.Companion.QS_EXPANDED
@@ -71,6 +72,7 @@ import com.android.keyguard.InternalFaceAuthReasons.USER_SWITCHING
NOTIFICATION_PANEL_CLICKED,
QS_EXPANDED,
PICK_UP_GESTURE_TRIGGERED,
+ ACCESSIBILITY_ACTION,
)
annotation class FaceAuthApiRequestReason {
companion object {
@@ -80,6 +82,7 @@ annotation class FaceAuthApiRequestReason {
const val QS_EXPANDED = "Face auth due to QS expansion."
const val PICK_UP_GESTURE_TRIGGERED =
"Face auth due to pickup gesture triggered when the device is awake and not from AOD."
+ const val ACCESSIBILITY_ACTION = "Face auth due to an accessibility action."
}
}
@@ -217,7 +220,8 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) :
@UiEvent(doc = STRONG_AUTH_ALLOWED_CHANGED)
FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED(1255, STRONG_AUTH_ALLOWED_CHANGED),
@UiEvent(doc = NON_STRONG_BIOMETRIC_ALLOWED_CHANGED)
- FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED);
+ FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED),
+ @UiEvent(doc = ACCESSIBILITY_ACTION) FACE_AUTH_ACCESSIBILITY_ACTION(1454, ACCESSIBILITY_ACTION);
override fun getId(): Int = this.id
@@ -233,6 +237,8 @@ private val apiRequestReasonToUiEvent =
FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED,
QS_EXPANDED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED,
PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED,
+ PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED,
+ ACCESSIBILITY_ACTION to FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION,
)
/** Converts the [reason] to the corresponding [FaceAuthUiEvent]. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt
index 50e5466d0325..1cb8e43cf2c8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockFrame.kt
@@ -12,12 +12,9 @@ class KeyguardClockFrame(
) : FrameLayout(context, attrs) {
private var drawAlpha: Int = 255
- init {
- setLayerType(View.LAYER_TYPE_SOFTWARE, null)
- }
-
protected override fun onSetAlpha(alpha: Int): Boolean {
- drawAlpha = alpha
+ // Ignore alpha passed from View, prefer to compute it from set values
+ drawAlpha = (255 * this.alpha * transitionAlpha).toInt()
return true
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
index 2377057f1fc5..d9b7bde66c67 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -69,7 +69,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView {
(long) (125 * KeyguardPatternView.DISAPPEAR_MULTIPLIER_LOCKED),
0.6f /* translationScale */,
0.45f /* delayScale */, AnimationUtils.loadInterpolator(
- mContext, android.R.interpolator.fast_out_linear_in));
+ mContext, android.R.interpolator.fast_out_linear_in));
mDisappearYTranslation = getResources().getDimensionPixelSize(
R.dimen.disappear_y_translation);
mYTrans = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
@@ -82,8 +82,10 @@ public class KeyguardPINView extends KeyguardPinBasedInputView {
}
void onDevicePostureChanged(@DevicePostureInt int posture) {
- mLastDevicePosture = posture;
- updateMargins();
+ if (mLastDevicePosture != posture) {
+ mLastDevicePosture = posture;
+ updateMargins();
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 38c07dc98471..2bdf46e1309d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -104,8 +104,10 @@ public class KeyguardPatternView extends KeyguardInputView
}
void onDevicePostureChanged(@DevicePostureInt int posture) {
- mLastDevicePosture = posture;
- updateMargins();
+ if (mLastDevicePosture != posture) {
+ mLastDevicePosture = posture;
+ updateMargins();
+ }
}
private void updateMargins() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index f9523370adb1..bc24249b23c9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -68,6 +68,7 @@ import com.android.keyguard.dagger.KeyguardBouncerScope;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.R;
+import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate;
import com.android.systemui.biometrics.SideFpsController;
import com.android.systemui.biometrics.SideFpsUiRequestSource;
import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
@@ -417,9 +418,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
BouncerMessageInteractor bouncerMessageInteractor,
Provider<JavaAdapter> javaAdapter,
UserInteractor userInteractor,
+ FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
Provider<SceneInteractor> sceneInteractor
) {
super(view);
+ view.setAccessibilityDelegate(faceAuthAccessibilityDelegate);
mLockPatternUtils = lockPatternUtils;
mUpdateMonitor = keyguardUpdateMonitor;
mSecurityModel = keyguardSecurityModel;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index a6252a39ee8a..75852793c24d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -113,6 +113,7 @@ public class KeyguardStatusView extends GridLayout {
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardStatusView:");
pw.println(" mDarkAmount: " + mDarkAmount);
+ pw.println(" visibility: " + getVisibility());
if (mClockView != null) {
mClockView.dump(pw, args);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 6854c97c3415..a04d13b93ddd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -37,15 +37,19 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
+import androidx.viewpager.widget.ViewPager;
import com.android.app.animation.Interpolators;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.keyguard.KeyguardClockSwitch.ClockSize;
import com.android.keyguard.logging.KeyguardLogger;
+import com.android.systemui.Dumpable;
import com.android.systemui.R;
+import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ClockController;
import com.android.systemui.statusbar.notification.AnimatableProperty;
@@ -58,14 +62,17 @@ import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ViewController;
+import java.io.PrintWriter;
+
import javax.inject.Inject;
/**
* Injectable controller for {@link KeyguardStatusView}.
*/
-public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> {
+public class KeyguardStatusViewController extends ViewController<KeyguardStatusView> implements
+ Dumpable {
private static final boolean DEBUG = KeyguardConstants.DEBUG;
- private static final String TAG = "KeyguardStatusViewController";
+ @VisibleForTesting static final String TAG = "KeyguardStatusViewController";
/**
* Duration to use for the animator when the keyguard status view alignment changes, and a
@@ -87,6 +94,8 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
private Boolean mStatusViewCentered = true;
+ private DumpManager mDumpManager;
+
private final TransitionListenerAdapter mKeyguardStatusAlignmentTransitionListener =
new TransitionListenerAdapter() {
@Override
@@ -112,7 +121,8 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
ScreenOffAnimationController screenOffAnimationController,
KeyguardLogger logger,
FeatureFlags featureFlags,
- InteractionJankMonitor interactionJankMonitor) {
+ InteractionJankMonitor interactionJankMonitor,
+ DumpManager dumpManager) {
super(keyguardStatusView);
mKeyguardSliceViewController = keyguardSliceViewController;
mKeyguardClockSwitchController = keyguardClockSwitchController;
@@ -123,11 +133,13 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
logger.getBuffer());
mInteractionJankMonitor = interactionJankMonitor;
mFeatureFlags = featureFlags;
+ mDumpManager = dumpManager;
}
@Override
public void onInit() {
mKeyguardClockSwitchController.init();
+ mDumpManager.registerDumpable(this);
}
@Override
@@ -143,6 +155,13 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
}
/**
+ * Called in notificationPanelViewController to avoid leak
+ */
+ public void onDestroy() {
+ mDumpManager.unregisterDumpable(TAG);
+ }
+
+ /**
* Updates views on doze time tick.
*/
public void dozeTimeTick() {
@@ -365,6 +384,19 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
// Excluding media from the transition on split-shade, as it doesn't transition
// horizontally properly.
transition.excludeTarget(R.id.status_view_media_container, true);
+
+ // Exclude smartspace viewpager and its children from the transition.
+ // - Each step of the transition causes the ViewPager to invoke resize,
+ // which invokes scrolling to the recalculated position. The scrolling
+ // actions are congested, resulting in kinky translation, and
+ // delay in settling to the final position. (http://b/281620564#comment1)
+ // - Also, the scrolling is unnecessary in the transition. We just want
+ // the viewpager to stay on the same page.
+ // - Exclude by Class type instead of resource id, since the resource id
+ // isn't available for all devices, and probably better to exclude all
+ // ViewPagers any way.
+ transition.excludeTarget(ViewPager.class, true);
+ transition.excludeChildren(ViewPager.class, true);
}
transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
@@ -397,6 +429,24 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
adapter.setDuration(KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION);
adapter.addTarget(clockView);
set.addTransition(adapter);
+
+ if (splitShadeEnabled) {
+ // Exclude smartspace viewpager and its children from the transition set.
+ // - This is necessary in addition to excluding them from the
+ // ChangeBounds child transition.
+ // - Without this, the viewpager is scrolled to the new position
+ // (corresponding to its end size) before the size change is realized.
+ // Note that the size change is realized at the end of the ChangeBounds
+ // transition. With the "prescrolling", the viewpager ends up in a weird
+ // position, then recovers smoothly during the transition, and ends at
+ // the position for the current page.
+ // - Exclude by Class type instead of resource id, since the resource id
+ // isn't available for all devices, and probably better to exclude all
+ // ViewPagers any way.
+ set.excludeTarget(ViewPager.class, true);
+ set.excludeChildren(ViewPager.class, true);
+ }
+
set.addListener(mKeyguardStatusAlignmentTransitionListener);
TransitionManager.beginDelayedTransition(notifContainerParent, set);
}
@@ -408,6 +458,11 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV
constraintSet.applyTo(notifContainerParent);
}
+ @Override
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ mView.dump(pw, args);
+ }
+
@VisibleForTesting
static class SplitShadeTransitionAdapter extends Transition {
private static final String PROP_BOUNDS_LEFT = "splitShadeTransitionAdapter:boundsLeft";
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 8f03eede1b1e..f1cb37c0ae76 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -164,13 +164,13 @@ import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.domain.interactor.FaceAuthenticationListener;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
import com.android.systemui.keyguard.shared.constants.TrustAgentUiEvent;
-import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.DetectionStatus;
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus;
-import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.AcquiredFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus;
+import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus;
+import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus;
import com.android.systemui.keyguard.shared.model.SysUiFaceAuthenticateOptions;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.WeatherData;
@@ -1471,28 +1471,32 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
private FaceAuthenticationListener mFaceAuthenticationListener =
new FaceAuthenticationListener() {
@Override
- public void onAuthenticationStatusChanged(@NonNull AuthenticationStatus status) {
- if (status instanceof AcquiredAuthenticationStatus) {
+ public void onAuthenticationStatusChanged(
+ @NonNull FaceAuthenticationStatus status
+ ) {
+ if (status instanceof AcquiredFaceAuthenticationStatus) {
handleFaceAcquired(
- ((AcquiredAuthenticationStatus) status).getAcquiredInfo());
- } else if (status instanceof ErrorAuthenticationStatus) {
- ErrorAuthenticationStatus error = (ErrorAuthenticationStatus) status;
+ ((AcquiredFaceAuthenticationStatus) status).getAcquiredInfo());
+ } else if (status instanceof ErrorFaceAuthenticationStatus) {
+ ErrorFaceAuthenticationStatus error =
+ (ErrorFaceAuthenticationStatus) status;
handleFaceError(error.getMsgId(), error.getMsg());
- } else if (status instanceof FailedAuthenticationStatus) {
+ } else if (status instanceof FailedFaceAuthenticationStatus) {
handleFaceAuthFailed();
- } else if (status instanceof HelpAuthenticationStatus) {
- HelpAuthenticationStatus helpMsg = (HelpAuthenticationStatus) status;
+ } else if (status instanceof HelpFaceAuthenticationStatus) {
+ HelpFaceAuthenticationStatus helpMsg =
+ (HelpFaceAuthenticationStatus) status;
handleFaceHelp(helpMsg.getMsgId(), helpMsg.getMsg());
- } else if (status instanceof SuccessAuthenticationStatus) {
+ } else if (status instanceof SuccessFaceAuthenticationStatus) {
FaceManager.AuthenticationResult result =
- ((SuccessAuthenticationStatus) status).getSuccessResult();
+ ((SuccessFaceAuthenticationStatus) status).getSuccessResult();
handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric());
}
}
@Override
- public void onDetectionStatusChanged(@NonNull DetectionStatus status) {
- handleFaceAuthenticated(status.getUserId(), status.isStrongBiometric());
+ public void onDetectionStatusChanged(@NonNull FaceDetectionStatus status) {
+ handleBiometricDetected(status.getUserId(), FACE, status.isStrongBiometric());
}
};
@@ -3154,6 +3158,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
return false;
}
+ if (isFaceAuthInteractorEnabled()) {
+ return mFaceAuthInteractor.canFaceAuthRun();
+ }
+
final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive
&& !statusBarShadeLocked;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 71f78c32317c..3990b10267e1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -31,6 +31,7 @@ import com.android.systemui.statusbar.notification.stack.AnimationProperties;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.Assert;
import com.google.errorprone.annotations.CompileTimeConstant;
@@ -85,6 +86,7 @@ public class KeyguardVisibilityHelper {
boolean keyguardFadingAway,
boolean goingToFullShade,
int oldStatusBarState) {
+ Assert.isMainThread();
PropertyAnimator.cancelAnimation(mView, AnimatableProperty.ALPHA);
boolean isOccluded = mKeyguardStateController.isOccluded();
mKeyguardViewVisibilityAnimating = false;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 2f6a68c3ff8d..03ad132c452c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -54,6 +54,7 @@ import com.android.systemui.recents.Recents;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -188,6 +189,7 @@ public class SystemActions implements CoreStartable {
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
private final NotificationShadeWindowController mNotificationShadeController;
private final ShadeController mShadeController;
+ private final Lazy<ShadeViewController> mShadeViewController;
private final StatusBarWindowCallback mNotificationShadeCallback;
private boolean mDismissNotificationShadeActionRegistered;
@@ -196,12 +198,14 @@ public class SystemActions implements CoreStartable {
UserTracker userTracker,
NotificationShadeWindowController notificationShadeController,
ShadeController shadeController,
+ Lazy<ShadeViewController> shadeViewController,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
Optional<Recents> recentsOptional,
DisplayTracker displayTracker) {
mContext = context;
mUserTracker = userTracker;
mShadeController = shadeController;
+ mShadeViewController = shadeViewController;
mRecentsOptional = recentsOptional;
mDisplayTracker = displayTracker;
mReceiver = new SystemActionsBroadcastReceiver();
@@ -330,8 +334,7 @@ public class SystemActions implements CoreStartable {
final Optional<CentralSurfaces> centralSurfacesOptional =
mCentralSurfacesOptionalLazy.get();
if (centralSurfacesOptional.isPresent()
- && centralSurfacesOptional.get().getShadeViewController() != null
- && centralSurfacesOptional.get().getShadeViewController().isPanelExpanded()
+ && mShadeViewController.get().isPanelExpanded()
&& !centralSurfacesOptional.get().isKeyguardShowing()) {
if (!mDismissNotificationShadeActionRegistered) {
mA11yManager.registerSystemAction(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
index 783460c325fa..0ef256d41157 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
@@ -145,6 +145,8 @@ class FontScalingDialog(
*/
@MainThread
fun updateFontScaleDelayed(delayMsFromSource: Long) {
+ doneButton.isEnabled = false
+
var delayMs = delayMsFromSource
if (systemClock.elapsedRealtime() - lastUpdateTime < MIN_UPDATE_INTERVAL_MS) {
delayMs += MIN_UPDATE_INTERVAL_MS
@@ -197,17 +199,22 @@ class FontScalingDialog(
title.post {
title.setTextAppearance(R.style.TextAppearance_Dialog_Title)
doneButton.setTextAppearance(R.style.Widget_Dialog_Button)
+ doneButton.isEnabled = true
}
}
}
@WorkerThread
fun updateFontScale() {
- systemSettings.putStringForUser(
- Settings.System.FONT_SCALE,
- strEntryValues[lastProgress.get()],
- userTracker.userId
- )
+ if (
+ !systemSettings.putStringForUser(
+ Settings.System.FONT_SCALE,
+ strEntryValues[lastProgress.get()],
+ userTracker.userId
+ )
+ ) {
+ title.post { doneButton.isEnabled = true }
+ }
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index a9779663cc7c..deb3d035d753 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
package com.android.systemui.authentication.data.repository
import com.android.internal.widget.LockPatternChecker
@@ -29,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.time.SystemClock
import dagger.Binds
import dagger.Module
@@ -38,16 +37,14 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** Defines interface for classes that can access authentication-related application state. */
@@ -156,32 +153,18 @@ constructor(
}
override val isAutoConfirmEnabled: StateFlow<Boolean> =
- userRepository.selectedUserInfo
- .map { it.id }
- .flatMapLatest { userId ->
- flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) }
- .flowOn(backgroundDispatcher)
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
+ refreshingFlow(
+ initialValue = false,
+ getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled,
+ )
override val hintedPinLength: Int = 6
override val isPatternVisible: StateFlow<Boolean> =
- userRepository.selectedUserInfo
- .map { it.id }
- .flatMapLatest { userId ->
- flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) }
- .flowOn(backgroundDispatcher)
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = true,
- )
+ refreshingFlow(
+ initialValue = true,
+ getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
+ )
private val _throttling = MutableStateFlow(AuthenticationThrottlingModel())
override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow()
@@ -276,6 +259,48 @@ constructor(
)
}
}
+
+ /**
+ * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is
+ * invoked on a background thread every time the selected user is changed and every time a new
+ * downstream subscriber is added to the flow.
+ *
+ * Initially, the flow will emit [initialValue] while it refreshes itself in the background by
+ * invoking the [getFreshValue] function and emitting the fresh value when that's done.
+ *
+ * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the
+ * new value.
+ *
+ * Every time a new downstream subscriber is added to the flow it first receives the latest
+ * cached value that's either the [initialValue] or the latest previously fetched value. In
+ * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a
+ * subsequent emission of that newest value.
+ */
+ private fun <T> refreshingFlow(
+ initialValue: T,
+ getFreshValue: suspend (selectedUserId: Int) -> T,
+ ): StateFlow<T> {
+ val flow = MutableStateFlow(initialValue)
+ applicationScope.launch {
+ combine(
+ // Emits a value initially and every time the selected user is changed.
+ userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
+ // Emits a value only when the number of downstream subscribers of this flow
+ // increases.
+ flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current)
+ ->
+ current > previous
+ },
+ ) { selectedUserId, _ ->
+ selectedUserId
+ }
+ .collect { selectedUserId ->
+ flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) }
+ }
+ }
+
+ return flow.asStateFlow()
+ }
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index b482977bde67..d4371bf30e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -104,7 +104,9 @@ constructor(
}
.stateIn(
scope = applicationScope,
- started = SharingStarted.Eagerly,
+ // Make sure this is kept as WhileSubscribed or we can run into a bug where the
+ // downstream continues to receive old/stale/cached values.
+ started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 9df56fcce430..58adfa1d882c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -58,6 +58,10 @@ import android.widget.ScrollView;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
+import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
@@ -333,6 +337,20 @@ public class AuthContainerView extends LinearLayout
addView(mFrameLayout);
mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
mBackgroundView = mFrameLayout.findViewById(R.id.background);
+ ViewCompat.setAccessibilityDelegate(mBackgroundView, new AccessibilityDelegateCompat() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(
+ new AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ mContext.getString(R.string.biometric_dialog_cancel_authentication)
+ )
+ );
+ }
+ });
+
mPanelView = mFrameLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
new file mode 100644
index 000000000000..b9fa24022ad5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import com.android.keyguard.FaceAuthApiRequestReason
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import javax.inject.Inject
+
+/**
+ * Accessibility delegate that will add a click accessibility action to a view when face auth can
+ * run. When the click a11y action is triggered, face auth will retry.
+ */
+@SysUISingleton
+class FaceAuthAccessibilityDelegate
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val faceAuthInteractor: KeyguardFaceAuthInteractor,
+) : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ if (keyguardUpdateMonitor.shouldListenForFace()) {
+ val clickActionToRetryFace =
+ AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+ resources.getString(R.string.retry_face)
+ )
+ info.addAction(clickActionToRetryFace)
+ }
+ }
+
+ override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+ return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) {
+ keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION)
+ faceAuthInteractor.onAccessibilityAction()
+ true
+ } else super.performAccessibilityAction(host, action, args)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 083e21fbdfba..37ce44488346 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -17,7 +17,12 @@ package com.android.systemui.biometrics
import android.app.ActivityTaskManager
import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -28,23 +33,27 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController
import android.os.Handler
import android.util.Log
+import android.util.RotationUtils
import android.view.Display
import android.view.DisplayInfo
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
+import android.view.View.AccessibilityDelegate
import android.view.ViewPropertyAnimator
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.RawRes
import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -55,7 +64,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.traceSection
import java.io.PrintWriter
import javax.inject.Inject
-import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -78,7 +86,6 @@ constructor(
@Main private val mainExecutor: DelayableExecutor,
@Main private val handler: Handler,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
- private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
@Application private val scope: CoroutineScope,
dumpManager: DumpManager
) : Dumpable {
@@ -243,15 +250,105 @@ constructor(
private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
overlayView = view
- SideFpsOverlayViewBinder.bind(
- view = view,
- viewModel = sideFpsOverlayViewModelFactory.get(),
- overlayViewParams = overlayViewParams,
- reason = reason,
- context = context,
+ val display = context.display!!
+ // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
+ display.getDisplayInfo(displayInfo)
+ val offsets =
+ sensorProps.getLocation(display.uniqueId).let { location ->
+ if (location == null) {
+ Log.w(TAG, "No location specified for display: ${display.uniqueId}")
+ }
+ location ?: sensorProps.location
+ }
+ overlayOffsets = offsets
+
+ val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+ view.rotation =
+ display.asSideFpsAnimationRotation(
+ offsets.isYAligned(),
+ getRotationFromDefault(displayInfo.rotation)
+ )
+ lottie.setAnimation(
+ display.asSideFpsAnimation(
+ offsets.isYAligned(),
+ getRotationFromDefault(displayInfo.rotation)
+ )
)
+ lottie.addLottieOnCompositionLoadedListener {
+ // Check that view is not stale, and that overlayView has not been hidden/removed
+ if (overlayView != null && overlayView == view) {
+ updateOverlayParams(display, it.bounds)
+ }
+ }
orientationReasonListener.reason = reason
+ lottie.addOverlayDynamicColor(context, reason)
+
+ /**
+ * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
+ * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
+ * in focus
+ */
+ view.setAccessibilityDelegate(
+ object : AccessibilityDelegate() {
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent
+ ): Boolean {
+ return if (
+ event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ ) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
+ )
}
+
+ @VisibleForTesting
+ fun updateOverlayParams(display: Display, bounds: Rect) {
+ val isNaturalOrientation = display.isNaturalOrientation()
+ val isDefaultOrientation =
+ if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
+ val size = windowManager.maximumWindowMetrics.bounds
+
+ val displayWidth = if (isDefaultOrientation) size.width() else size.height()
+ val displayHeight = if (isDefaultOrientation) size.height() else size.width()
+ val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
+ val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
+
+ val sensorBounds =
+ if (overlayOffsets.isYAligned()) {
+ Rect(
+ displayWidth - boundsWidth,
+ overlayOffsets.sensorLocationY,
+ displayWidth,
+ overlayOffsets.sensorLocationY + boundsHeight
+ )
+ } else {
+ Rect(
+ overlayOffsets.sensorLocationX,
+ 0,
+ overlayOffsets.sensorLocationX + boundsWidth,
+ boundsHeight
+ )
+ }
+
+ RotationUtils.rotateBounds(
+ sensorBounds,
+ Rect(0, 0, displayWidth, displayHeight),
+ getRotationFromDefault(display.rotation)
+ )
+
+ overlayViewParams.x = sensorBounds.left
+ overlayViewParams.y = sensorBounds.top
+
+ windowManager.updateViewLayout(overlayView, overlayViewParams)
+ }
+
+ private fun getRotationFromDefault(rotation: Int): Int =
+ if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
}
private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
@@ -276,12 +373,89 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo
private fun ActivityTaskManager.topClass(): String =
getTasks(1).firstOrNull()?.topActivity?.className ?: ""
+@RawRes
+private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
+ when (rotationFromDefault) {
+ Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+ Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+ else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
+ }
+
+private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
+ when (rotationFromDefault) {
+ Surface.ROTATION_90 -> if (yAligned) 0f else 180f
+ Surface.ROTATION_180 -> 180f
+ Surface.ROTATION_270 -> if (yAligned) 180f else 0f
+ else -> 0f
+ }
+
private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
private fun Display.isNaturalOrientation(): Boolean =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-public class OrientationReasonListener(
+private fun LottieAnimationView.addOverlayDynamicColor(
+ context: Context,
+ @BiometricOverlayConstants.ShowReason reason: Int
+) {
+ fun update() {
+ val isKeyguard = reason == REASON_AUTH_KEYGUARD
+ if (isKeyguard) {
+ val color =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorPrimaryFixed
+ )
+ val outerRimColor =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorPrimaryFixedDim
+ )
+ val chevronFill =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorOnPrimaryFixed
+ )
+ addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
+ }
+ addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
+ }
+ addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
+ }
+ } else {
+ if (!isDarkMode(context)) {
+ addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
+ }
+ }
+ for (key in listOf(".blue600", ".blue400")) {
+ addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(
+ context.getColor(R.color.settingslib_color_blue400),
+ PorterDuff.Mode.SRC_ATOP
+ )
+ }
+ }
+ }
+ }
+
+ if (composition != null) {
+ update()
+ } else {
+ addLottieOnCompositionLoadedListener { update() }
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ return darkMode == Configuration.UI_MODE_NIGHT_YES
+}
+
+@VisibleForTesting
+class OrientationReasonListener(
context: Context,
displayManager: DisplayManager,
handler: Handler,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index a5e846ad61ca..53dc0e3d4846 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -17,6 +17,8 @@
package com.android.systemui.biometrics.dagger
import com.android.settingslib.udfps.UdfpsUtils
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.data.repository.FacePropertyRepositoryImpl
import com.android.systemui.biometrics.data.repository.FaceSettingsRepository
import com.android.systemui.biometrics.data.repository.FaceSettingsRepositoryImpl
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
@@ -53,6 +55,10 @@ interface BiometricsModule {
@Binds
@SysUISingleton
+ fun faceSensors(impl: FacePropertyRepositoryImpl): FacePropertyRepository
+
+ @Binds
+ @SysUISingleton
fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
new file mode 100644
index 000000000000..d2cb84945252
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorStrength
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+
+/** A repository for the global state of Face sensor. */
+interface FacePropertyRepository {
+ /** Face sensor information, null if it is not available. */
+ val sensorInfo: Flow<FaceSensorInfo?>
+}
+
+/** Describes a biometric sensor */
+data class FaceSensorInfo(val id: Int, val strength: SensorStrength)
+
+private const val TAG = "FaceSensorPropertyRepositoryImpl"
+
+@SysUISingleton
+class FacePropertyRepositoryImpl
+@Inject
+constructor(@Application private val applicationScope: CoroutineScope, faceManager: FaceManager?) :
+ FacePropertyRepository {
+
+ private val sensorProps: Flow<List<FaceSensorPropertiesInternal>> =
+ faceManager?.let {
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ val callback =
+ object : IFaceAuthenticatorsRegisteredCallback.Stub() {
+ override fun onAllAuthenticatorsRegistered(
+ sensors: List<FaceSensorPropertiesInternal>
+ ) {
+ trySendWithFailureLogging(
+ sensors,
+ TAG,
+ "onAllAuthenticatorsRegistered"
+ )
+ }
+ }
+ it.addAuthenticatorsRegisteredCallback(callback)
+ awaitClose {}
+ }
+ .shareIn(applicationScope, SharingStarted.Eagerly)
+ }
+ ?: flowOf(emptyList())
+
+ override val sensorInfo: Flow<FaceSensorInfo?> =
+ sensorProps
+ .map { it.firstOrNull() }
+ .map { it?.let { FaceSensorInfo(it.sensorId, it.sensorStrength.toSensorStrength()) } }
+}
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 efbde4c5985b..daff5feb0123 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
@@ -16,15 +16,13 @@
package com.android.systemui.biometrics.data.repository
-import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintManager
-import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorStrength
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -33,8 +31,10 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn
/**
@@ -44,17 +44,22 @@ import kotlinx.coroutines.flow.shareIn
*/
interface FingerprintPropertyRepository {
+ /**
+ * If the repository is initialized or not. Other properties are defaults until this is true.
+ */
+ val isInitialized: Flow<Boolean>
+
/** The id of fingerprint sensor. */
- val sensorId: Flow<Int>
+ val sensorId: StateFlow<Int>
/** The security strength of sensor (convenience, weak, strong). */
- val strength: Flow<SensorStrength>
+ val strength: StateFlow<SensorStrength>
/** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
- val sensorType: Flow<FingerprintSensorType>
+ val sensorType: StateFlow<FingerprintSensorType>
/** The sensor location relative to each physical display. */
- val sensorLocations: Flow<Map<String, SensorLocationInternal>>
+ val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
}
@SysUISingleton
@@ -62,10 +67,10 @@ class FingerprintPropertyRepositoryImpl
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val fingerprintManager: FingerprintManager?
+ private val fingerprintManager: FingerprintManager?,
) : FingerprintPropertyRepository {
- private val props: Flow<FingerprintSensorPropertiesInternal> =
+ override val isInitialized: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -73,56 +78,45 @@ constructor(
sensors: List<FingerprintSensorPropertiesInternal>
) {
if (sensors.isNotEmpty()) {
- trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
- } else {
- trySendWithFailureLogging(
- DEFAULT_PROPS,
- TAG,
- "initialize with default properties"
- )
+ setProperties(sensors[0])
+ trySendWithFailureLogging(true, TAG, "initialize properties")
}
}
}
fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
- trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
+ trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
awaitClose {}
}
.shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
- override val sensorId: Flow<Int> = props.map { it.sensorId }
- override val strength: Flow<SensorStrength> =
- props.map { sensorStrengthIntToObject(it.sensorStrength) }
- override val sensorType: Flow<FingerprintSensorType> =
- props.map { sensorTypeIntToObject(it.sensorType) }
- override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
- props.map {
- it.allLocations.associateBy { sensorLocationInternal ->
+ private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
+ override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
+
+ private val _strength: MutableStateFlow<SensorStrength> =
+ MutableStateFlow(SensorStrength.CONVENIENCE)
+ override val strength = _strength.asStateFlow()
+
+ private val _sensorType: MutableStateFlow<FingerprintSensorType> =
+ MutableStateFlow(FingerprintSensorType.UNKNOWN)
+ override val sensorType = _sensorType.asStateFlow()
+
+ private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
+ MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
+ override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+ _sensorLocations.asStateFlow()
+
+ private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
+ _sensorId.value = prop.sensorId
+ _strength.value = prop.sensorStrength.toSensorStrength()
+ _sensorType.value = sensorTypeIntToObject(prop.sensorType)
+ _sensorLocations.value =
+ prop.allLocations.associateBy { sensorLocationInternal ->
sensorLocationInternal.displayId
}
- }
+ }
companion object {
private const val TAG = "FingerprintPropertyRepositoryImpl"
- private val DEFAULT_PROPS =
- FingerprintSensorPropertiesInternal(
- -1 /* sensorId */,
- SensorProperties.STRENGTH_CONVENIENCE,
- 0 /* maxEnrollmentsPerUser */,
- listOf<ComponentInfoInternal>(),
- FingerprintSensorProperties.TYPE_UNKNOWN,
- false /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
- )
- }
-}
-
-private fun sensorStrengthIntToObject(value: Int): SensorStrength {
- return when (value) {
- 0 -> SensorStrength.CONVENIENCE
- 1 -> SensorStrength.WEAK
- 2 -> SensorStrength.STRONG
- else -> throw IllegalArgumentException("Invalid SensorStrength value: $value")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
index 37f39cb5fe0e..aa85e5f3b21a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
@@ -17,24 +17,16 @@
package com.android.systemui.biometrics.domain.interactor
import android.hardware.biometrics.SensorLocationInternal
+import android.util.Log
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
/** Business logic for SideFps overlay offsets. */
interface SideFpsOverlayInteractor {
- /** The displayId of the current display. */
- val displayId: Flow<String>
- /** The corresponding offsets based on different displayId. */
- val overlayOffsets: Flow<SensorLocationInternal>
-
- /** Update the displayId. */
- fun changeDisplay(displayId: String?)
+ /** Get the corresponding offsets based on different displayId. */
+ fun getOverlayOffsets(displayId: String): SensorLocationInternal
}
@SysUISingleton
@@ -43,16 +35,14 @@ class SideFpsOverlayInteractorImpl
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
SideFpsOverlayInteractor {
- private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
- override val displayId: Flow<String> = _displayId.asStateFlow()
-
- override val overlayOffsets: Flow<SensorLocationInternal> =
- combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
- offsets[displayId] ?: SensorLocationInternal.DEFAULT
+ override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
+ val offsets = fingerprintPropertyRepository.sensorLocations.value
+ return if (offsets.containsKey(displayId)) {
+ offsets[displayId]!!
+ } else {
+ Log.w(TAG, "No location specified for display: $displayId")
+ offsets[""]!!
}
-
- override fun changeDisplay(displayId: String?) {
- _displayId.value = displayId ?: ""
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
index 2982d0be3764..30e865eff8b8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
@@ -18,9 +18,18 @@ package com.android.systemui.biometrics.shared.model
import android.hardware.biometrics.SensorProperties
-/** Fingerprint sensor security strength. Represents [SensorProperties.Strength]. */
+/** Sensor security strength. Represents [SensorProperties.Strength]. */
enum class SensorStrength {
CONVENIENCE,
WEAK,
STRONG,
}
+
+/** Convert [this] to corresponding [SensorStrength] */
+fun Int.toSensorStrength(): SensorStrength =
+ when (this) {
+ 0 -> SensorStrength.CONVENIENCE
+ 1 -> SensorStrength.WEAK
+ 2 -> SensorStrength.STRONG
+ else -> throw IllegalArgumentException("Invalid SensorStrength value: $this")
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index e5a4d1a644f1..9bbf1ef04481 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -305,6 +305,10 @@ object BiometricViewBinder {
.collect { onClick ->
iconViewOverlay.setOnClickListener(onClick)
iconView.setOnClickListener(onClick)
+ if (onClick == null) {
+ iconViewOverlay.isClickable = false
+ iconView.isClickable = false
+ }
}
}
@@ -479,10 +483,10 @@ private class Spaghetti(
failureReason,
messageAfterError = modalities.asDefaultHelpMessage(applicationContext),
authenticateAfterError = modalities.hasFingerprint,
- suppressIf = { currentMessage ->
+ suppressIf = { currentMessage, history ->
modalities.hasFaceAndFingerprint &&
failedModality == BiometricModality.Face &&
- currentMessage.isError
+ (currentMessage.isError || history.faceFailed)
},
failedModality = failedModality,
)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
deleted file mode 100644
index 0409519c9816..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.binder
-
-import android.content.Context
-import android.content.res.Configuration
-import android.graphics.Color
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
-import android.hardware.biometrics.BiometricOverlayConstants
-import android.view.View
-import android.view.WindowManager
-import android.view.accessibility.AccessibilityEvent
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.model.KeyPath
-import com.android.systemui.R
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.launch
-
-/** Sub-binder for SideFpsOverlayView. */
-object SideFpsOverlayViewBinder {
-
- /** Bind the view. */
- @JvmStatic
- fun bind(
- view: View,
- viewModel: SideFpsOverlayViewModel,
- overlayViewParams: WindowManager.LayoutParams,
- @BiometricOverlayConstants.ShowReason reason: Int,
- @Application context: Context
- ) {
- val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
- val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
-
- viewModel.changeDisplay()
-
- view.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
- viewModel.sideFpsAnimationRotation.collect { rotation ->
- view.rotation = rotation
- }
- }
-
- launch {
- // TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation
- // in order to add scuba tests in the future.
- viewModel.sideFpsAnimation.collect { animation ->
- lottie.setAnimation(animation)
- }
- }
-
- launch {
- viewModel.sensorBounds.collect { sensorBounds ->
- overlayViewParams.x = sensorBounds.left
- overlayViewParams.y = sensorBounds.top
-
- windowManager.updateViewLayout(view, overlayViewParams)
- }
- }
-
- launch {
- viewModel.overlayOffsets.collect { overlayOffsets ->
- lottie.addLottieOnCompositionLoadedListener {
- viewModel.updateSensorBounds(
- it.bounds,
- windowManager.maximumWindowMetrics.bounds,
- overlayOffsets
- )
- }
- }
- }
- }
- }
-
- lottie.addOverlayDynamicColor(context, reason)
-
- /**
- * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
- * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
- * in focus
- */
- view.accessibilityDelegate =
- object : View.AccessibilityDelegate() {
- override fun dispatchPopulateAccessibilityEvent(
- host: View,
- event: AccessibilityEvent
- ): Boolean {
- return if (
- event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- ) {
- true
- } else {
- super.dispatchPopulateAccessibilityEvent(host, event)
- }
- }
- }
- }
-}
-
-private fun LottieAnimationView.addOverlayDynamicColor(
- context: Context,
- @BiometricOverlayConstants.ShowReason reason: Int
-) {
- fun update() {
- val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD
- if (isKeyguard) {
- val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color
- val chevronFill =
- com.android.settingslib.Utils.getColorAttrDefaultColor(
- context,
- android.R.attr.textColorPrimaryInverse
- )
- for (key in listOf(".blue600", ".blue400")) {
- addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
- }
- }
- addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
- }
- } else if (!isDarkMode(context)) {
- addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
- }
- } else if (isDarkMode(context)) {
- for (key in listOf(".blue600", ".blue400")) {
- addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
- PorterDuffColorFilter(
- context.getColor(R.color.settingslib_color_blue400),
- PorterDuff.Mode.SRC_ATOP
- )
- }
- }
- }
- }
-
- if (composition != null) {
- update()
- } else {
- addLottieOnCompositionLoadedListener { update() }
- }
-}
-
-private fun isDarkMode(context: Context): Boolean {
- val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
- return darkMode == Configuration.UI_MODE_NIGHT_YES
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistory.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistory.kt
new file mode 100644
index 000000000000..d002bf02cc04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistory.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import com.android.systemui.biometrics.shared.model.BiometricModality
+
+/** Contains metadata about key events that have occurred while biometric prompt is showing. */
+interface PromptHistory {
+
+ /** If face authentication has failed at least once. */
+ val faceFailed: Boolean
+
+ /** If fingerprint authentication has failed at least once. */
+ val fingerprintFailed: Boolean
+}
+
+class PromptHistoryImpl : PromptHistory {
+ private var failures = mutableSetOf<BiometricModality>()
+
+ override val faceFailed
+ get() = failures.contains(BiometricModality.Face)
+
+ override val fingerprintFailed
+ get() = failures.contains(BiometricModality.Fingerprint)
+
+ /** Record a failure event. */
+ fun failure(modality: BiometricModality) {
+ if (modality != BiometricModality.None) {
+ failures.add(modality)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 8a2e4059ee73..dca19c503fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -204,6 +204,7 @@ constructor(
}
.distinctUntilChanged()
+ private val history = PromptHistoryImpl()
private var messageJob: Job? = null
/**
@@ -214,13 +215,13 @@ constructor(
* is set (or via [showHelp] when not set) after the error is dismissed.
*
* The error is ignored if the user has already authenticated or if [suppressIf] is true given
- * the currently showing [PromptMessage].
+ * the currently showing [PromptMessage] and [PromptHistory].
*/
suspend fun showTemporaryError(
message: String,
messageAfterError: String,
authenticateAfterError: Boolean,
- suppressIf: (PromptMessage) -> Boolean = { false },
+ suppressIf: (PromptMessage, PromptHistory) -> Boolean = { _, _ -> false },
hapticFeedback: Boolean = true,
failedModality: BiometricModality = BiometricModality.None,
) = coroutineScope {
@@ -230,7 +231,9 @@ constructor(
_canTryAgainNow.value = supportsRetry(failedModality)
- if (suppressIf(_message.value)) {
+ val suppress = suppressIf(_message.value, history)
+ history.failure(failedModality)
+ if (suppress) {
return@coroutineScope
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
deleted file mode 100644
index e938b4efb68c..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.viewmodel
-
-import android.content.Context
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.util.RotationUtils
-import android.view.Display
-import android.view.DisplayInfo
-import android.view.Surface
-import androidx.annotation.RawRes
-import com.android.systemui.R
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.dagger.qualifiers.Application
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.map
-
-/** View-model for SideFpsOverlayView. */
-class SideFpsOverlayViewModel
-@Inject
-constructor(
- @Application private val context: Context,
- private val sideFpsOverlayInteractor: SideFpsOverlayInteractor,
-) {
-
- private val isReverseDefaultRotation =
- context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
-
- private val _sensorBounds: MutableStateFlow<Rect> = MutableStateFlow(Rect())
- val sensorBounds = _sensorBounds.asStateFlow()
-
- val overlayOffsets: Flow<SensorLocationInternal> = sideFpsOverlayInteractor.overlayOffsets
-
- /** Update the displayId. */
- fun changeDisplay() {
- sideFpsOverlayInteractor.changeDisplay(context.display!!.uniqueId)
- }
-
- /** Determine the rotation of the sideFps animation given the overlay offsets. */
- val sideFpsAnimationRotation: Flow<Float> =
- overlayOffsets.map { overlayOffsets ->
- val display = context.display!!
- val displayInfo: DisplayInfo = DisplayInfo()
- // b/284098873 `context.display.rotation` may not up-to-date, we use
- // displayInfo.rotation
- display.getDisplayInfo(displayInfo)
- val yAligned: Boolean = overlayOffsets.isYAligned()
- when (getRotationFromDefault(displayInfo.rotation)) {
- Surface.ROTATION_90 -> if (yAligned) 0f else 180f
- Surface.ROTATION_180 -> 180f
- Surface.ROTATION_270 -> if (yAligned) 180f else 0f
- else -> 0f
- }
- }
-
- /** Populate the sideFps animation from the overlay offsets. */
- @RawRes
- val sideFpsAnimation: Flow<Int> =
- overlayOffsets.map { overlayOffsets ->
- val display = context.display!!
- val displayInfo: DisplayInfo = DisplayInfo()
- // b/284098873 `context.display.rotation` may not up-to-date, we use
- // displayInfo.rotation
- display.getDisplayInfo(displayInfo)
- val yAligned: Boolean = overlayOffsets.isYAligned()
- when (getRotationFromDefault(displayInfo.rotation)) {
- Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
- Surface.ROTATION_180 ->
- if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
- else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
- }
- }
-
- /**
- * Calculate and update the bounds of the sensor based on the bounds of the overlay view, the
- * maximum bounds of the window, and the offsets of the sensor location.
- */
- fun updateSensorBounds(
- bounds: Rect,
- maximumWindowBounds: Rect,
- offsets: SensorLocationInternal
- ) {
- val isNaturalOrientation = context.display!!.isNaturalOrientation()
- val isDefaultOrientation =
- if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
-
- val displayWidth =
- if (isDefaultOrientation) maximumWindowBounds.width() else maximumWindowBounds.height()
- val displayHeight =
- if (isDefaultOrientation) maximumWindowBounds.height() else maximumWindowBounds.width()
- val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
- val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
-
- val sensorBounds =
- if (offsets.isYAligned()) {
- Rect(
- displayWidth - boundsWidth,
- offsets.sensorLocationY,
- displayWidth,
- offsets.sensorLocationY + boundsHeight
- )
- } else {
- Rect(
- offsets.sensorLocationX,
- 0,
- offsets.sensorLocationX + boundsWidth,
- boundsHeight
- )
- }
-
- val displayInfo: DisplayInfo = DisplayInfo()
- context.display!!.getDisplayInfo(displayInfo)
-
- RotationUtils.rotateBounds(
- sensorBounds,
- Rect(0, 0, displayWidth, displayHeight),
- getRotationFromDefault(displayInfo.rotation)
- )
-
- _sensorBounds.value = sensorBounds
- }
-
- private fun getRotationFromDefault(rotation: Int): Int =
- if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
-}
-
-private fun Display.isNaturalOrientation(): Boolean =
- rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-
-private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index 2abdb849cd9a..e3e9b3a3754a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -16,10 +16,10 @@
package com.android.systemui.bouncer.domain.interactor
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.time.SystemClock
@@ -35,8 +35,8 @@ constructor(
private val keyguardStateController: KeyguardStateController,
private val bouncerRepository: KeyguardBouncerRepository,
private val biometricSettingsRepository: BiometricSettingsRepository,
- private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
private val systemClock: SystemClock,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
) {
var receivedDownTouch = false
val isVisible: Flow<Boolean> = bouncerRepository.alternateBouncerVisible
@@ -78,7 +78,7 @@ constructor(
biometricSettingsRepository.isFingerprintEnrolled.value &&
biometricSettingsRepository.isStrongBiometricAllowed.value &&
biometricSettingsRepository.isFingerprintEnabledByDevicePolicy.value &&
- !deviceEntryFingerprintAuthRepository.isLockedOut.value &&
+ !keyguardUpdateMonitor.isFingerprintLockedOut &&
!keyguardStateController.isUnlocked &&
!statusBarStateController.isDozing
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index 1b14acc7fabc..844cf024ef71 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -40,9 +40,10 @@ class PinBouncerViewModel(
) {
val pinShapes = PinShapeAdapter(applicationContext)
+ private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty())
- private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList())
- val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries
+ /** Currently entered pin keys. */
+ val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
/** The length of the PIN for which we should show a hint. */
val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength
@@ -50,17 +51,19 @@ class PinBouncerViewModel(
/** Appearance of the backspace button. */
val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
combine(
- mutablePinEntries,
+ mutablePinInput,
interactor.isAutoConfirmEnabled,
) { mutablePinEntries, isAutoConfirmEnabled ->
computeBackspaceButtonAppearance(
- enteredPin = mutablePinEntries,
+ pinInput = mutablePinEntries,
isAutoConfirmEnabled = isAutoConfirmEnabled,
)
}
.stateIn(
scope = applicationScope,
- started = SharingStarted.Eagerly,
+ // Make sure this is kept as WhileSubscribed or we can run into a bug where the
+ // downstream continues to receive old/stale/cached values.
+ started = SharingStarted.WhileSubscribed(),
initialValue = ActionButtonAppearance.Hidden,
)
@@ -87,26 +90,23 @@ class PinBouncerViewModel(
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
- if (mutablePinEntries.value.isEmpty()) {
+ val pinInput = mutablePinInput.value
+ if (pinInput.isEmpty()) {
interactor.clearMessage()
}
- mutablePinEntries.value += EnteredKey(input)
-
+ mutablePinInput.value = pinInput.append(input)
tryAuthenticate(useAutoConfirm = true)
}
/** Notifies that the user clicked the backspace button. */
fun onBackspaceButtonClicked() {
- if (mutablePinEntries.value.isEmpty()) {
- return
- }
- mutablePinEntries.value = mutablePinEntries.value.toMutableList().apply { removeLast() }
+ mutablePinInput.value = mutablePinInput.value.deleteLast()
}
/** Notifies that the user long-pressed the backspace button. */
fun onBackspaceButtonLongPressed() {
- mutablePinEntries.value = emptyList()
+ mutablePinInput.value = mutablePinInput.value.clearAll()
}
/** Notifies that the user clicked the "enter" button. */
@@ -115,7 +115,7 @@ class PinBouncerViewModel(
}
private fun tryAuthenticate(useAutoConfirm: Boolean) {
- val pinCode = mutablePinEntries.value.map { it.input }
+ val pinCode = mutablePinInput.value.getPin()
applicationScope.launch {
val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch
@@ -124,15 +124,17 @@ class PinBouncerViewModel(
showFailureAnimation()
}
- mutablePinEntries.value = emptyList()
+ // TODO(b/291528545): this should not be cleared on success (at least until the view
+ // is animated away).
+ mutablePinInput.value = mutablePinInput.value.clearAll()
}
}
private fun computeBackspaceButtonAppearance(
- enteredPin: List<EnteredKey>,
+ pinInput: PinInputViewModel,
isAutoConfirmEnabled: Boolean,
): ActionButtonAppearance {
- val isEmpty = enteredPin.isEmpty()
+ val isEmpty = pinInput.isEmpty()
return when {
isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden
@@ -151,19 +153,3 @@ enum class ActionButtonAppearance {
/** Button is shown. */
Shown,
}
-
-private var nextSequenceNumber = 1
-
-/**
- * The pin bouncer [input] as digits 0-9, together with a [sequenceNumber] to indicate the ordering.
- *
- * Since the model only allows appending/removing [EnteredKey]s from the end, the [sequenceNumber]
- * is strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at
- * a specific number.
- */
-data class EnteredKey
-internal constructor(val input: Int, val sequenceNumber: Int = nextSequenceNumber++) :
- Comparable<EnteredKey> {
- override fun compareTo(other: EnteredKey): Int =
- compareValuesBy(this, other, EnteredKey::sequenceNumber)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt
new file mode 100644
index 000000000000..4efc21b41e6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.viewmodel
+
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+
+/**
+ * Immutable pin input state.
+ *
+ * The input is a hybrid of state ([Digit]) and event ([ClearAll]) tokens. The [ClearAll] token can
+ * be interpreted as a watermark, indicating that the current input up to that point is deleted
+ * (after a auth failure or when long-pressing the delete button). Therefore, [Digit]s following a
+ * [ClearAll] make up the next pin input entry. Up to two complete pin inputs are memoized.
+ *
+ * This is required when auto-confirm rejects the input, and the last digit will be animated-in at
+ * the end of the input, concurrently with the staggered clear-all animation starting to play at the
+ * beginning of the input.
+ *
+ * The input is guaranteed to always contain a initial [ClearAll] token as a sentinel, thus clients
+ * can always assume there is a 'ClearAll' watermark available.
+ */
+data class PinInputViewModel
+internal constructor(
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val input: List<EntryToken>
+) {
+ init {
+ require(input.firstOrNull() is ClearAll) { "input does not begin with a ClearAll token" }
+ require(input.zipWithNext().all { it.first < it.second }) {
+ "EntryTokens are not sorted by their sequenceNumber"
+ }
+ }
+ /**
+ * [PinInputViewModel] with [previousInput] and appended [newToken].
+ *
+ * [previousInput] is trimmed so that the new [PinBouncerViewModel] contains at most two pin
+ * inputs.
+ */
+ private constructor(
+ previousInput: List<EntryToken>,
+ newToken: EntryToken
+ ) : this(
+ buildList {
+ addAll(
+ previousInput.subList(previousInput.indexOfLastClearAllToKeep(), previousInput.size)
+ )
+ add(newToken)
+ }
+ )
+
+ fun append(digit: Int): PinInputViewModel {
+ return PinInputViewModel(input, Digit(digit))
+ }
+
+ /**
+ * Delete last digit.
+ *
+ * This removes the last digit from the input. Returns `this` if the last token is [ClearAll].
+ */
+ fun deleteLast(): PinInputViewModel {
+ if (isEmpty()) return this
+ return PinInputViewModel(input.take(input.size - 1))
+ }
+
+ /**
+ * Appends a [ClearAll] watermark, completing the current pin.
+ *
+ * Returns `this` if the last token is [ClearAll].
+ */
+ fun clearAll(): PinInputViewModel {
+ if (isEmpty()) return this
+ return PinInputViewModel(input, ClearAll())
+ }
+
+ /** Whether the current pin is empty. */
+ fun isEmpty(): Boolean {
+ return input.last() is ClearAll
+ }
+
+ /** The current pin, or an empty list if [isEmpty]. */
+ fun getPin(): List<Int> {
+ return getDigits(mostRecentClearAll()).map { it.input }
+ }
+
+ /**
+ * The digits following the specified [ClearAll] marker, up to the next marker or the end of the
+ * input.
+ *
+ * Returns an empty list if the [ClearAll] is not in the input.
+ */
+ fun getDigits(clearAllMarker: ClearAll): List<Digit> {
+ val startIndex = input.indexOf(clearAllMarker) + 1
+ if (startIndex == 0 || startIndex == input.size) return emptyList()
+
+ return input.subList(startIndex, input.size).takeWhile { it is Digit }.map { it as Digit }
+ }
+
+ /** The most recent [ClearAll] marker. */
+ fun mostRecentClearAll(): ClearAll {
+ return input.last { it is ClearAll } as ClearAll
+ }
+
+ companion object {
+ fun empty() = PinInputViewModel(listOf(ClearAll()))
+ }
+}
+
+/**
+ * Pin bouncer entry token with a [sequenceNumber] to indicate input event ordering.
+ *
+ * Since the model only allows appending/removing [Digit]s from the end, the [sequenceNumber] is
+ * strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at a
+ * specific number.
+ */
+sealed interface EntryToken : Comparable<EntryToken> {
+ val sequenceNumber: Int
+
+ /** The pin bouncer [input] as digits 0-9. */
+ data class Digit
+ internal constructor(val input: Int, override val sequenceNumber: Int = nextSequenceNumber++) :
+ EntryToken {
+ init {
+ check(input in 0..9)
+ }
+ }
+
+ /**
+ * Marker to indicate the input is completely cleared, and subsequent [EntryToken]s mark a new
+ * pin entry.
+ */
+ data class ClearAll
+ internal constructor(override val sequenceNumber: Int = nextSequenceNumber++) : EntryToken
+
+ override fun compareTo(other: EntryToken): Int =
+ compareValuesBy(this, other, EntryToken::sequenceNumber)
+
+ companion object {
+ private var nextSequenceNumber = 1
+ }
+}
+
+/**
+ * Index of the last [ClearAll] token to keep for a new [PinInputViewModel], so that after appending
+ * another [EntryToken], there are at most two pin inputs in the [PinInputViewModel].
+ */
+private fun List<EntryToken>.indexOfLastClearAllToKeep(): Int {
+ require(isNotEmpty() && first() is ClearAll)
+
+ var seenClearAll = 0
+ for (i in size - 1 downTo 0) {
+ if (get(i) is ClearAll) {
+ seenClearAll++
+ if (seenClearAll == 2) {
+ return i
+ }
+ }
+ }
+
+ // The first element is guaranteed to be a ClearAll marker.
+ return 0
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
index e342ac2f320d..566a74ae3e07 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java
@@ -17,6 +17,7 @@
package com.android.systemui.clipboardoverlay;
import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -41,10 +42,16 @@ class IntentCreator {
// From the ACTION_SEND docs:
// "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the
// MIME type of the data in EXTRA_STREAM"
- if (clipData.getItemAt(0).getUri() != null) {
- shareIntent.setDataAndType(
- clipData.getItemAt(0).getUri(), clipData.getDescription().getMimeType(0));
- shareIntent.putExtra(Intent.EXTRA_STREAM, clipData.getItemAt(0).getUri());
+ Uri uri = clipData.getItemAt(0).getUri();
+ if (uri != null) {
+ // We don't use setData here because some apps interpret this as "to:".
+ shareIntent.setType(clipData.getDescription().getMimeType(0));
+ // Include URI in ClipData also, so that grantPermission picks it up.
+ shareIntent.setClipData(new ClipData(
+ new ClipDescription(
+ "content", new String[]{clipData.getDescription().getMimeType(0)}),
+ new ClipData.Item(uri)));
+ shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
shareIntent.putExtra(
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index b15c60e62ead..85f31e5e6b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -21,13 +21,11 @@ import android.content.Context
import android.view.View
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.util.time.SystemClock
/**
* A facade to interact with Compose, when it is available.
@@ -64,13 +62,6 @@ interface BaseComposeFacade {
): View
/** Create a [View] to represent [viewModel] on screen. */
- fun createMultiShadeView(
- context: Context,
- viewModel: MultiShadeViewModel,
- clock: SystemClock,
- ): View
-
- /** Create a [View] to represent [viewModel] on screen. */
fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 35cf4a1ecf0a..35624770b712 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -43,8 +43,6 @@ import com.android.systemui.rotationlock.RotationLockModule;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
-import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeControllerImpl;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.KeyboardShortcutsModule;
@@ -150,9 +148,6 @@ public abstract class ReferenceSystemUIModule {
@Binds
abstract DockManager bindDockManager(DockManagerImpl dockManager);
- @Binds
- abstract ShadeController provideShadeController(ShadeControllerImpl shadeController);
-
@SysUISingleton
@Provides
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index d82bf587212e..6fdb4ca7238f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -223,10 +223,10 @@ public interface SysUIComponent {
Optional<NaturalRotationUnfoldProgressProvider> getNaturalRotationUnfoldProgressProvider();
/** */
- Optional<MediaMuteAwaitConnectionCli> getMediaMuteAwaitConnectionCli();
+ MediaMuteAwaitConnectionCli getMediaMuteAwaitConnectionCli();
/** */
- Optional<NearbyMediaDevicesManager> getNearbyMediaDevicesManager();
+ NearbyMediaDevicesManager getNearbyMediaDevicesManager();
/**
* Returns {@link CoreStartable}s that should be started with the application.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index a560accfff68..d9665c5b5047 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -48,6 +48,7 @@ import com.android.systemui.reardisplay.RearDisplayDialogController
import com.android.systemui.recents.Recents
import com.android.systemui.settings.dagger.MultiUserUtilsModule
import com.android.systemui.shortcut.ShortcutKeyDispatcher
+import com.android.systemui.statusbar.ImmersiveModeConfirmation
import com.android.systemui.statusbar.notification.InstantAppNotifier
import com.android.systemui.statusbar.phone.KeyguardLiftController
import com.android.systemui.statusbar.phone.LockscreenWallpaper
@@ -162,6 +163,12 @@ abstract class SystemUICoreStartableModule {
@ClassKey(Recents::class)
abstract fun bindRecents(sysui: Recents): CoreStartable
+ /** Inject into ImmersiveModeConfirmation. */
+ @Binds
+ @IntoMap
+ @ClassKey(ImmersiveModeConfirmation::class)
+ abstract fun bindImmersiveModeConfirmation(sysui: ImmersiveModeConfirmation): CoreStartable
+
/** Inject into RingtonePlayer. */
@Binds
@IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
index 0e224060a36f..f3a07fc53027 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamLogger.kt
@@ -16,15 +16,52 @@
package com.android.systemui.dreams
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.log.dagger.DreamLog
-import javax.inject.Inject
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
/** Logs dream-related stuff to a {@link LogBuffer}. */
-class DreamLogger @Inject constructor(@DreamLog private val buffer: LogBuffer) {
- /** Logs a debug message to the buffer. */
- fun d(tag: String, message: String) {
- buffer.log(tag, LogLevel.DEBUG, { str1 = message }, { message })
- }
+class DreamLogger(buffer: MessageBuffer, tag: String) : Logger(buffer, tag) {
+ fun logDreamOverlayEnabled(enabled: Boolean) =
+ d({ "Dream overlay enabled: $bool1" }) { bool1 = enabled }
+
+ fun logIgnoreAddComplication(reason: String, complication: String) =
+ d({ "Ignore adding complication, reason: $str1, complication: $str2" }) {
+ str1 = reason
+ str2 = complication
+ }
+
+ fun logIgnoreRemoveComplication(reason: String, complication: String) =
+ d({ "Ignore removing complication, reason: $str1, complication: $str2" }) {
+ str1 = reason
+ str2 = complication
+ }
+
+ fun logAddComplication(complication: String) =
+ d({ "Add dream complication: $str1" }) { str1 = complication }
+
+ fun logRemoveComplication(complication: String) =
+ d({ "Remove dream complication: $str1" }) { str1 = complication }
+
+ fun logOverlayActive(active: Boolean) = d({ "Dream overlay active: $bool1" }) { bool1 = active }
+
+ fun logLowLightActive(active: Boolean) =
+ d({ "Low light mode active: $bool1" }) { bool1 = active }
+
+ fun logHasAssistantAttention(hasAttention: Boolean) =
+ d({ "Dream overlay has Assistant attention: $bool1" }) { bool1 = hasAttention }
+
+ fun logStatusBarVisible(visible: Boolean) =
+ d({ "Dream overlay status bar visible: $bool1" }) { bool1 = visible }
+
+ fun logAvailableComplicationTypes(types: Int) =
+ d({ "Available complication types: $int1" }) { int1 = types }
+
+ fun logShouldShowComplications(showComplications: Boolean) =
+ d({ "Dream overlay should show complications: $bool1" }) { bool1 = showComplications }
+
+ fun logShowOrHideStatusBarItem(show: Boolean, type: String) =
+ d({ "${if (bool1) "Showing" else "Hiding"} dream status bar item: $int1" }) {
+ bool1 = show
+ str1 = type
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index 484bf3d51f36..01fb5227749f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -36,6 +36,9 @@ import com.android.systemui.complication.ComplicationLayoutParams.Position
import com.android.systemui.dreams.dagger.DreamOverlayModule
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.DreamLog
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -65,12 +68,14 @@ constructor(
private val mDreamInTranslationYDistance: Int,
@Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION)
private val mDreamInTranslationYDurationMs: Long,
- private val mLogger: DreamLogger,
+ @DreamLog logBuffer: LogBuffer,
) {
companion object {
private const val TAG = "DreamOverlayAnimationsController"
}
+ private val logger = Logger(logBuffer, TAG)
+
private var mAnimator: Animator? = null
private lateinit var view: View
@@ -179,11 +184,11 @@ constructor(
doOnEnd {
mAnimator = null
mOverlayStateController.setEntryAnimationsFinished(true)
- mLogger.d(TAG, "Dream overlay entry animations finished.")
+ logger.d("Dream overlay entry animations finished.")
}
- doOnCancel { mLogger.d(TAG, "Dream overlay entry animations canceled.") }
+ doOnCancel { logger.d("Dream overlay entry animations canceled.") }
start()
- mLogger.d(TAG, "Dream overlay entry animations started.")
+ logger.d("Dream overlay entry animations started.")
}
}
@@ -242,11 +247,11 @@ constructor(
doOnEnd {
mAnimator = null
mOverlayStateController.setExitAnimationsRunning(false)
- mLogger.d(TAG, "Dream overlay exit animations finished.")
+ logger.d("Dream overlay exit animations finished.")
}
- doOnCancel { mLogger.d(TAG, "Dream overlay exit animations canceled.") }
+ doOnCancel { logger.d("Dream overlay exit animations canceled.") }
start()
- mLogger.d(TAG, "Dream overlay exit animations started.")
+ logger.d("Dream overlay exit animations started.")
}
mOverlayStateController.setExitAnimationsRunning(true)
return mAnimator as AnimatorSet
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index c2421dcbc6ca..c9748f954670 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -28,6 +28,8 @@ import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.dagger.DreamLog;
import com.android.systemui.statusbar.policy.CallbackController;
import java.util.ArrayList;
@@ -115,10 +117,10 @@ public class DreamOverlayStateController implements
public DreamOverlayStateController(@Main Executor executor,
@Named(DREAM_OVERLAY_ENABLED) boolean overlayEnabled,
FeatureFlags featureFlags,
- DreamLogger dreamLogger) {
+ @DreamLog LogBuffer logBuffer) {
mExecutor = executor;
mOverlayEnabled = overlayEnabled;
- mLogger = dreamLogger;
+ mLogger = new DreamLogger(logBuffer, TAG);
mFeatureFlags = featureFlags;
if (mFeatureFlags.isEnabled(Flags.ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS)) {
mSupportedTypes = Complication.COMPLICATION_TYPE_NONE
@@ -126,7 +128,7 @@ public class DreamOverlayStateController implements
} else {
mSupportedTypes = Complication.COMPLICATION_TYPE_NONE;
}
- mLogger.d(TAG, "Dream overlay enabled: " + mOverlayEnabled);
+ mLogger.logDreamOverlayEnabled(mOverlayEnabled);
}
/**
@@ -134,14 +136,13 @@ public class DreamOverlayStateController implements
*/
public void addComplication(Complication complication) {
if (!mOverlayEnabled) {
- mLogger.d(TAG,
- "Ignoring adding complication due to overlay disabled: " + complication);
+ mLogger.logIgnoreAddComplication("overlay disabled", complication.toString());
return;
}
mExecutor.execute(() -> {
if (mComplications.add(complication)) {
- mLogger.d(TAG, "Added dream complication: " + complication);
+ mLogger.logAddComplication(complication.toString());
mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
}
});
@@ -152,14 +153,13 @@ public class DreamOverlayStateController implements
*/
public void removeComplication(Complication complication) {
if (!mOverlayEnabled) {
- mLogger.d(TAG,
- "Ignoring removing complication due to overlay disabled: " + complication);
+ mLogger.logIgnoreRemoveComplication("overlay disabled", complication.toString());
return;
}
mExecutor.execute(() -> {
if (mComplications.remove(complication)) {
- mLogger.d(TAG, "Removed dream complication: " + complication);
+ mLogger.logRemoveComplication(complication.toString());
mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
}
});
@@ -305,7 +305,7 @@ public class DreamOverlayStateController implements
* @param active {@code true} if overlay is active, {@code false} otherwise.
*/
public void setOverlayActive(boolean active) {
- mLogger.d(TAG, "Dream overlay active: " + active);
+ mLogger.logOverlayActive(active);
modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_ACTIVE);
}
@@ -314,7 +314,7 @@ public class DreamOverlayStateController implements
* @param active {@code true} if low light mode is active, {@code false} otherwise.
*/
public void setLowLightActive(boolean active) {
- mLogger.d(TAG, "Low light mode active: " + active);
+ mLogger.logLowLightActive(active);
if (isLowLightActive() && !active) {
// Notify that we're exiting low light only on the transition from active to not active.
@@ -346,7 +346,7 @@ public class DreamOverlayStateController implements
* @param hasAttention {@code true} if has the user's attention, {@code false} otherwise.
*/
public void setHasAssistantAttention(boolean hasAttention) {
- mLogger.d(TAG, "Dream overlay has Assistant attention: " + hasAttention);
+ mLogger.logHasAssistantAttention(hasAttention);
modifyState(hasAttention ? OP_SET_STATE : OP_CLEAR_STATE, STATE_HAS_ASSISTANT_ATTENTION);
}
@@ -355,7 +355,7 @@ public class DreamOverlayStateController implements
* @param visible {@code true} if the status bar is visible, {@code false} otherwise.
*/
public void setDreamOverlayStatusBarVisible(boolean visible) {
- mLogger.d(TAG, "Dream overlay status bar visible: " + visible);
+ mLogger.logStatusBarVisible(visible);
modifyState(
visible ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE);
}
@@ -373,7 +373,7 @@ public class DreamOverlayStateController implements
*/
public void setAvailableComplicationTypes(@Complication.ComplicationType int types) {
mExecutor.execute(() -> {
- mLogger.d(TAG, "Available complication types: " + types);
+ mLogger.logAvailableComplicationTypes(types);
mAvailableComplicationTypes = types;
mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged);
});
@@ -391,7 +391,7 @@ public class DreamOverlayStateController implements
*/
public void setShouldShowComplications(boolean shouldShowComplications) {
mExecutor.execute(() -> {
- mLogger.d(TAG, "Should show complications: " + shouldShowComplications);
+ mLogger.logShouldShowComplications(shouldShowComplications);
mShouldShowComplications = shouldShowComplications;
mCallbacks.forEach(Callback::onAvailableComplicationTypesChanged);
});
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 3a284083e844..a6401b6594ba 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -36,6 +36,8 @@ import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.dagger.DreamLog;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CrossFadeHelper;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
@@ -161,7 +163,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
DreamOverlayStateController dreamOverlayStateController,
UserTracker userTracker,
- DreamLogger dreamLogger) {
+ @DreamLog LogBuffer logBuffer) {
super(view);
mResources = resources;
mMainExecutor = mainExecutor;
@@ -177,7 +179,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
mZenModeController = zenModeController;
mDreamOverlayStateController = dreamOverlayStateController;
mUserTracker = userTracker;
- mLogger = dreamLogger;
+ mLogger = new DreamLogger(logBuffer, TAG);
// Register to receive show/hide updates for the system status bar. Our custom status bar
// needs to hide when the system status bar is showing to ovoid overlapping status bars.
@@ -346,8 +348,8 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
@Nullable String contentDescription) {
mMainExecutor.execute(() -> {
if (mIsAttached) {
- mLogger.d(TAG, (show ? "Showing" : "Hiding") + " dream status bar item: "
- + DreamOverlayStatusBarView.getLoggableStatusIconType(iconType));
+ mLogger.logShowOrHideStatusBarItem(
+ show, DreamOverlayStatusBarView.getLoggableStatusIconType(iconType));
mView.showIcon(iconType, show, contentDescription);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
index 99451f2ee1b9..6f05e83b22ba 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
@@ -37,12 +37,15 @@ import javax.inject.Named;
*/
public class ShadeTouchHandler implements DreamTouchHandler {
private final Optional<CentralSurfaces> mSurfaces;
+ private final ShadeViewController mShadeViewController;
private final int mInitiationHeight;
@Inject
ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces,
+ ShadeViewController shadeViewController,
@Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) {
mSurfaces = centralSurfaces;
+ mShadeViewController = shadeViewController;
mInitiationHeight = initiationHeight;
}
@@ -54,12 +57,7 @@ public class ShadeTouchHandler implements DreamTouchHandler {
}
session.registerInputListener(ev -> {
- final ShadeViewController viewController =
- mSurfaces.map(CentralSurfaces::getShadeViewController).orElse(null);
-
- if (viewController != null) {
- viewController.handleExternalTouch((MotionEvent) ev);
- }
+ mShadeViewController.handleExternalTouch((MotionEvent) ev);
if (ev instanceof MotionEvent) {
if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index add323983928..66813f90bac4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -77,7 +77,7 @@ object Flags {
// TODO(b/278873737): Tracking Bug
@JvmField
val LOAD_NOTIFICATIONS_BEFORE_THE_USER_SWITCH_IS_COMPLETE =
- releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
+ releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete")
// TODO(b/277338665): Tracking Bug
@JvmField
@@ -92,11 +92,7 @@ object Flags {
// TODO(b/288326013): Tracking Bug
@JvmField
val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION =
- unreleasedFlag(
- 288326013,
- "notification_async_hybrid_view_inflation",
- teamfood = false
- )
+ unreleasedFlag(288326013, "notification_async_hybrid_view_inflation", teamfood = false)
@JvmField
val ANIMATED_NOTIFICATION_SHADE_INSETS =
@@ -104,18 +100,23 @@ object Flags {
// TODO(b/268005230): Tracking Bug
@JvmField
- val SENSITIVE_REVEAL_ANIM =
- unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
+ val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
// TODO(b/280783617): Tracking Bug
@Keep
@JvmField
val BUILDER_EXTRAS_OVERRIDE =
- sysPropBooleanFlag(
- 128,
- "persist.sysui.notification.builder_extras_override",
- default = false
- )
+ sysPropBooleanFlag(
+ 128,
+ "persist.sysui.notification.builder_extras_override",
+ default = true
+ )
+
+ /** Only notify group expansion listeners when a change happens. */
+ // TODO(b/292213543): Tracking Bug
+ @JvmField
+ val NOTIFICATION_GROUP_EXPANSION_CHANGE =
+ unreleasedFlag(292213543, "notification_group_expansion_change", teamfood = false)
// 200 - keyguard/lockscreen
// ** Flag retired **
@@ -133,16 +134,17 @@ object Flags {
// TODO(b/254512676): Tracking Bug
@JvmField
- val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag(
- 207,
- R.bool.config_enableLockScreenCustomClocks,
- "lockscreen_custom_clocks"
- )
+ val LOCKSCREEN_CUSTOM_CLOCKS =
+ resourceBooleanFlag(
+ 207,
+ R.bool.config_enableLockScreenCustomClocks,
+ "lockscreen_custom_clocks"
+ )
// TODO(b/275694445): Tracking Bug
@JvmField
- val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = releasedFlag(208,
- "lockscreen_without_secure_lock_when_dreaming")
+ val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING =
+ releasedFlag(208, "lockscreen_without_secure_lock_when_dreaming")
// TODO(b/286092087): Tracking Bug
@JvmField
@@ -156,8 +158,7 @@ object Flags {
* Whether the clock on a wide lock screen should use the new "stepping" animation for moving
* the digits when the clock moves.
*/
- @JvmField
- val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
+ @JvmField val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation")
/**
* Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
@@ -183,13 +184,11 @@ object Flags {
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
// TODO(b/262780002): Tracking Bug
- @JvmField
- val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
+ @JvmField val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui")
// flag for controlling auto pin confirmation and material u shapes in bouncer
@JvmField
- val AUTO_PIN_CONFIRMATION =
- releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
+ val AUTO_PIN_CONFIRMATION = releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation")
// TODO(b/262859270): Tracking Bug
@JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded")
@@ -206,20 +205,11 @@ object Flags {
/** Whether the long-press gesture to open wallpaper picker is enabled. */
// TODO(b/266242192): Tracking Bug
@JvmField
- val LOCK_SCREEN_LONG_PRESS_ENABLED =
- releasedFlag(
- 228,
- "lock_screen_long_press_enabled"
- )
+ val LOCK_SCREEN_LONG_PRESS_ENABLED = releasedFlag(228, "lock_screen_long_press_enabled")
/** Enables UI updates for AI wallpapers in the wallpaper picker. */
// TODO(b/267722622): Tracking Bug
- @JvmField
- val WALLPAPER_PICKER_UI_FOR_AIWP =
- releasedFlag(
- 229,
- "wallpaper_picker_ui_for_aiwp"
- )
+ @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag(229, "wallpaper_picker_ui_for_aiwp")
/** Whether to use a new data source for intents to run on keyguard dismissal. */
// TODO(b/275069969): Tracking bug.
@@ -232,6 +222,12 @@ object Flags {
val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP =
unreleasedFlag(232, "lock_screen_long_press_directly_opens_wallpaper_picker")
+ /** Whether page transition animations in the wallpaper picker are enabled */
+ // TODO(b/291710220): Tracking bug.
+ @JvmField
+ val WALLPAPER_PICKER_PAGE_TRANSITIONS =
+ unreleasedFlag(291710220, "wallpaper_picker_page_transitions")
+
/** Whether to run the new udfps keyguard refactor code. */
// TODO(b/279440316): Tracking bug.
@JvmField
@@ -239,27 +235,21 @@ object Flags {
/** Provide new auth messages on the bouncer. */
// TODO(b/277961132): Tracking bug.
- @JvmField
- val REVAMPED_BOUNCER_MESSAGES =
- unreleasedFlag(234, "revamped_bouncer_messages")
+ @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages")
/** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */
// TODO(b/279794160): Tracking bug.
- @JvmField
- val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
-
+ @JvmField val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true)
/** Keyguard Migration */
/** Migrate the indication area to the new keyguard root view. */
// TODO(b/280067944): Tracking bug.
- @JvmField
- val MIGRATE_INDICATION_AREA = unreleasedFlag(236, "migrate_indication_area", teamfood = true)
+ @JvmField val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area")
/**
- * Migrate the bottom area to the new keyguard root view.
- * Because there is no such thing as a "bottom area" after this, this also breaks it up into
- * many smaller, modular pieces.
+ * Migrate the bottom area to the new keyguard root view. Because there is no such thing as a
+ * "bottom area" after this, this also breaks it up into many smaller, modular pieces.
*/
// TODO(b/290652751): Tracking bug.
@JvmField
@@ -268,31 +258,29 @@ object Flags {
/** Whether to listen for fingerprint authentication over keyguard occluding activities. */
// TODO(b/283260512): Tracking bug.
- @JvmField
- val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
+ @JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps")
/** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */
// TODO(b/286563884): Tracking bug
- @JvmField
- val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
+ @JvmField val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix")
// TODO(b/287268101): Tracking bug.
- @JvmField
- val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
+ @JvmField val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock")
/** Migrate the lock icon view to the new keyguard root view. */
// TODO(b/286552209): Tracking bug.
- @JvmField
- val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
+ @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
// TODO(b/288276738): Tracking bug.
- @JvmField
- val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
+ @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
/** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
// TODO(b/288074305): Tracking bug.
- @JvmField
- val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
+ @JvmField val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
+
+ /** Migrate the status view from the notification panel to keyguard root view. */
+ // TODO(b/291767565): Tracking bug.
+ @JvmField val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view")
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@@ -311,8 +299,7 @@ object Flags {
// TODO(b/270223352): Tracking Bug
@JvmField
- val HIDE_SMARTSPACE_ON_DREAM_OVERLAY =
- releasedFlag(404, "hide_smartspace_on_dream_overlay")
+ val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag(404, "hide_smartspace_on_dream_overlay")
// TODO(b/271460958): Tracking Bug
@JvmField
@@ -352,8 +339,7 @@ object Flags {
/** Enables Font Scaling Quick Settings tile */
// TODO(b/269341316): Tracking Bug
- @JvmField
- val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
+ @JvmField val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile")
/** Enables new QS Edit Mode visual refresh */
// TODO(b/269787742): Tracking Bug
@@ -362,23 +348,11 @@ object Flags {
// 600- status bar
- // TODO(b/256614753): Tracking Bug
- val NEW_STATUS_BAR_MOBILE_ICONS = releasedFlag(606, "new_status_bar_mobile_icons")
-
- // TODO(b/256614751): Tracking Bug
- val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND =
- unreleasedFlag(608, "new_status_bar_mobile_icons_backend", teamfood = true)
-
- // TODO(b/260881289): Tracking Bug
- val NEW_STATUS_BAR_ICONS_DEBUG_COLORING =
- unreleasedFlag(611, "new_status_bar_icons_debug_coloring")
-
// TODO(b/265892345): Tracking Bug
val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip")
// TODO(b/280426085): Tracking Bug
- @JvmField
- val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
+ @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository")
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
@@ -411,12 +385,6 @@ object Flags {
// TODO(b/254512502): Tracking Bug
val MEDIA_SESSION_ACTIONS = unreleasedFlag(901, "media_session_actions")
- // TODO(b/254512726): Tracking Bug
- val MEDIA_NEARBY_DEVICES = releasedFlag(903, "media_nearby_devices")
-
- // TODO(b/254512695): Tracking Bug
- val MEDIA_MUTE_AWAIT = releasedFlag(904, "media_mute_await")
-
// TODO(b/254512654): Tracking Bug
@JvmField val DREAM_MEDIA_COMPLICATION = unreleasedFlag(905, "dream_media_complication")
@@ -432,16 +400,9 @@ object Flags {
// TODO(b/263272731): Tracking Bug
val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = releasedFlag(910, "media_ttt_receiver_success_ripple")
- // TODO(b/265813373): Tracking Bug
- val MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE = releasedFlag(912, "media_ttt_dismiss_gesture")
-
// TODO(b/266157412): Tracking Bug
val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions")
- // TODO(b/266739309): Tracking Bug
- @JvmField
- val MEDIA_RECOMMENDATION_CARD_UPDATE = releasedFlag(914, "media_recommendation_card_update")
-
// TODO(b/267007629): Tracking Bug
val MEDIA_RESUME_PROGRESS = releasedFlag(915, "media_resume_progress")
@@ -459,8 +420,8 @@ object Flags {
// TODO(b/273509374): Tracking Bug
@JvmField
- val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006,
- "always_show_home_controls_on_dreams")
+ val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS =
+ releasedFlag(1006, "always_show_home_controls_on_dreams")
// 1100 - windowing
@Keep
@@ -508,11 +469,7 @@ object Flags {
@Keep
@JvmField
val ENABLE_PIP_KEEP_CLEAR_ALGORITHM =
- sysPropBooleanFlag(
- 1110,
- "persist.wm.debug.enable_pip_keep_clear_algorithm",
- default = true
- )
+ sysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", default = true)
// TODO(b/256873975): Tracking Bug
@JvmField
@@ -550,7 +507,8 @@ object Flags {
// TODO(b/273443374): Tracking Bug
@Keep
- @JvmField val LOCKSCREEN_LIVE_WALLPAPER =
+ @JvmField
+ val LOCKSCREEN_LIVE_WALLPAPER =
sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true)
// TODO(b/281648899): Tracking bug
@@ -565,7 +523,6 @@ object Flags {
val ENABLE_PIP2_IMPLEMENTATION =
sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
-
// 1200 - predictive back
@Keep
@JvmField
@@ -591,8 +548,7 @@ object Flags {
unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true)
// TODO(b/270987164): Tracking Bug
- @JvmField
- val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
+ @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features")
// TODO(b/263826204): Tracking Bug
@JvmField
@@ -615,8 +571,7 @@ object Flags {
unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
// TODO(b/273800936): Tracking Bug
- @JvmField
- val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
+ @JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common")
// 1300 - screenshots
// TODO(b/264916608): Tracking Bug
@@ -641,16 +596,12 @@ object Flags {
// 1700 - clipboard
@JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior")
// TODO(b/278714186) Tracking Bug
- @JvmField val CLIPBOARD_IMAGE_TIMEOUT =
- unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
+ @JvmField
+ val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true)
// TODO(b/279405451): Tracking Bug
@JvmField
val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions")
- // 1800 - shade container
- // TODO(b/265944639): Tracking Bug
- @JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade")
-
// TODO(b/283300105): Tracking Bug
@JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container")
@@ -660,19 +611,15 @@ object Flags {
// 2000 - device controls
@Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels")
- @JvmField
- val APP_PANELS_ALL_APPS_ALLOWED =
- releasedFlag(2001, "app_panels_all_apps_allowed")
+ @JvmField val APP_PANELS_ALL_APPS_ALLOWED = releasedFlag(2001, "app_panels_all_apps_allowed")
@JvmField
- val CONTROLS_MANAGEMENT_NEW_FLOWS =
- releasedFlag(2002, "controls_management_new_flows")
+ val CONTROLS_MANAGEMENT_NEW_FLOWS = releasedFlag(2002, "controls_management_new_flows")
// Enables removing app from Home control panel as a part of a new flow
// TODO(b/269132640): Tracking Bug
@JvmField
- val APP_PANELS_REMOVE_APPS_ALLOWED =
- releasedFlag(2003, "app_panels_remove_apps_allowed")
+ val APP_PANELS_REMOVE_APPS_ALLOWED = releasedFlag(2003, "app_panels_remove_apps_allowed")
// 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.)
// TODO(b/259264861): Tracking Bug
@@ -683,11 +630,9 @@ object Flags {
// 2300 - stylus
@JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used")
+ @JvmField val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
@JvmField
- val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui")
- @JvmField
- val ENABLE_USI_BATTERY_NOTIFICATIONS =
- releasedFlag(2302, "enable_usi_battery_notifications")
+ val ENABLE_USI_BATTERY_NOTIFICATIONS = releasedFlag(2302, "enable_usi_battery_notifications")
@JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education")
// 2400 - performance tools and debugging info
@@ -699,11 +644,10 @@ object Flags {
// TODO(b/283071711): Tracking bug
@JvmField
val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK =
- unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
+ unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock")
// TODO:(b/283203305): Tracking bug
- @JvmField
- val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
+ @JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock")
// 2700 - unfold transitions
// TODO(b/265764985): Tracking Bug
@@ -726,27 +670,21 @@ object Flags {
@JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout")
// TODO(b/259428678): Tracking Bug
- @JvmField
- val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
+ @JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator")
// TODO(b/277192623): Tracking Bug
- @JvmField
- val KEYBOARD_EDUCATION =
- unreleasedFlag(2603, "keyboard_education", teamfood = false)
+ @JvmField val KEYBOARD_EDUCATION = unreleasedFlag(2603, "keyboard_education", teamfood = false)
// TODO(b/277201412): Tracking Bug
@JvmField
- val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
- releasedFlag(2805, "split_shade_subpixel_optimization")
+ val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = releasedFlag(2805, "split_shade_subpixel_optimization")
// TODO(b/288868056): Tracking Bug
@JvmField
- val PARTIAL_SCREEN_SHARING_TASK_SWITCHER =
- unreleasedFlag(288868056, "pss_task_switcher")
+ val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag(288868056, "pss_task_switcher")
// TODO(b/278761837): Tracking Bug
- @JvmField
- val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
+ @JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter")
// 2900 - Zero Jank fixes. Naming convention is: zj_<bug number>_<cuj name>
@@ -762,34 +700,31 @@ object Flags {
unreleasedFlag(3000, name = "enable_lockscreen_wallpaper_dream")
// TODO(b/283084712): Tracking Bug
- @JvmField
- val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
+ @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations")
// TODO(b/283447257): Tracking bug
@JvmField
val BIGPICTURE_NOTIFICATION_LAZY_LOADING =
- unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
+ unreleasedFlag(283447257, "bigpicture_notification_lazy_loading")
// TODO(b/283740863): Tracking Bug
@JvmField
val ENABLE_NEW_PRIVACY_DIALOG =
- unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false)
+ unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = true)
// TODO(b/289573946): Tracking Bug
- @JvmField
- val PRECOMPUTED_TEXT =
- unreleasedFlag(289573946, "precomputed_text")
+ @JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text")
// 2900 - CentralSurfaces-related flags
// TODO(b/285174336): Tracking Bug
@JvmField
- val USE_REPOS_FOR_BOUNCER_SHOWING = unreleasedFlag(2900, "use_repos_for_bouncer_showing")
+ val USE_REPOS_FOR_BOUNCER_SHOWING =
+ unreleasedFlag(2900, "use_repos_for_bouncer_showing", teamfood = true)
// 3100 - Haptic interactions
// TODO(b/290213663): Tracking Bug
@JvmField
- val ONE_WAY_HAPTICS_API_MIGRATION =
- unreleasedFlag(3100, "oneway_haptics_api_migration")
+ val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag(3100, "oneway_haptics_api_migration")
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt b/packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt
new file mode 100644
index 000000000000..eaecda52a5a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/ViewRefactorFlag.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags
+
+import android.util.Log
+import com.android.systemui.Dependency
+
+/**
+ * This class promotes best practices for flag guarding System UI view refactors.
+ * * [isEnabled] allows changing an implementation.
+ * * [assertDisabled] allows authors to flag code as being "dead" when the flag gets enabled and
+ * ensure that it is not being invoked accidentally in the post-flag refactor.
+ * * [expectEnabled] allows authors to guard new code with a "safe" alternative when invoked on
+ * flag-disabled builds, but with a check that should crash eng builds or tests when the
+ * expectation is violated.
+ *
+ * The constructors prefer that you provide a [FeatureFlags] instance, but does not require it,
+ * falling back to [Dependency.get]. This fallback should ONLY be used to flag-guard code changes
+ * inside views where injecting flag values after initialization can be error-prone.
+ */
+class ViewRefactorFlag
+private constructor(
+ private val injectedFlags: FeatureFlags?,
+ private val flag: BooleanFlag,
+ private val readFlagValue: (FeatureFlags) -> Boolean
+) {
+ @JvmOverloads
+ constructor(
+ flags: FeatureFlags? = null,
+ flag: UnreleasedFlag
+ ) : this(flags, flag, { it.isEnabled(flag) })
+
+ @JvmOverloads
+ constructor(
+ flags: FeatureFlags? = null,
+ flag: ReleasedFlag
+ ) : this(flags, flag, { it.isEnabled(flag) })
+
+ /** Whether the flag is enabled. Called to switch between an old behavior and a new behavior. */
+ val isEnabled by lazy {
+ @Suppress("DEPRECATION")
+ val featureFlags = injectedFlags ?: Dependency.get(FeatureFlags::class.java)
+ readFlagValue(featureFlags)
+ }
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ *
+ * Example usage:
+ * ```
+ * public void setController(NotificationShelfController notificationShelfController) {
+ * mShelfRefactor.assertDisabled();
+ * mController = notificationShelfController;
+ * }
+ * ````
+ */
+ fun assertDisabled() = check(!isEnabled) { "Code path not supported when $flag is enabled." }
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ *
+ * Example usage:
+ * ```
+ * public void setShelfIcons(NotificationIconContainer icons) {
+ * if (mShelfRefactor.expectEnabled()) {
+ * mShelfIcons = icons;
+ * }
+ * }
+ * ```
+ */
+ fun expectEnabled(): Boolean {
+ if (!isEnabled) {
+ val message = "Code path not supported when $flag is disabled."
+ Log.wtf(TAG, message, Exception(message))
+ }
+ return isEnabled
+ }
+
+ private companion object {
+ private const val TAG = "ViewRefactorFlag"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 23f6fa6bbf23..03a270ebea68 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -25,19 +25,24 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
+import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManager
import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommandListener
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
/** Binds keyguard views on startup, and also exposes methods to allow rebinding if views change */
+@ExperimentalCoroutinesApi
@SysUISingleton
class KeyguardViewConfigurator
@Inject
@@ -51,11 +56,14 @@ constructor(
private val indicationController: KeyguardIndicationController,
private val keyguardLayoutManager: KeyguardLayoutManager,
private val keyguardLayoutManagerCommandListener: KeyguardLayoutManagerCommandListener,
+ private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
+ private val chipbarCoordinator: ChipbarCoordinator,
) : CoreStartable {
private var indicationAreaHandle: DisposableHandle? = null
override fun start() {
+ bindKeyguardRootView()
val notificationPanel =
notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup
bindIndicationArea(notificationPanel)
@@ -116,4 +124,13 @@ constructor(
}
}
}
+
+ private fun bindKeyguardRootView() {
+ KeyguardRootViewBinder.bind(
+ keyguardRootView,
+ featureFlags,
+ occludingAppDeviceEntryMessageViewModel,
+ chipbarCoordinator,
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 468d7606933e..5d7ea1cc5819 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -163,9 +163,11 @@ import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.SystemClock;
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
import com.android.wm.shell.keyguard.KeyguardTransitions;
import dagger.Lazy;
@@ -290,6 +292,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
public static final String SYS_BOOT_REASON_PROP = "sys.boot.reason.last";
public static final String REBOOT_MAINLINE_UPDATE = "reboot,mainline_update";
private final DreamOverlayStateController mDreamOverlayStateController;
+ private final JavaAdapter mJavaAdapter;
+ private final WallpaperRepository mWallpaperRepository;
/** The stream type that the lock sounds are tied to. */
private int mUiSoundsStreamType;
@@ -440,6 +444,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private final LockPatternUtils mLockPatternUtils;
private final BroadcastDispatcher mBroadcastDispatcher;
private boolean mKeyguardDonePending = false;
+ private boolean mUnlockingAndWakingFromDream = false;
private boolean mHideAnimationRun = false;
private boolean mHideAnimationRunning = false;
@@ -798,6 +803,25 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false);
mKeyguardDisplayManager.hide();
mUpdateMonitor.startBiometricWatchdog();
+
+ // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
+ // dreaming. It's time to wake up.
+ if (mUnlockingAndWakingFromDream) {
+ Log.d(TAG, "waking from dream after unlock");
+ mUnlockingAndWakingFromDream = false;
+
+ if (mKeyguardStateController.isShowing()) {
+ Log.d(TAG, "keyguard showing after keyguardGone, dismiss");
+ mKeyguardViewControllerLazy.get()
+ .notifyKeyguardAuthenticated(!mWakeAndUnlocking);
+ } else {
+ Log.d(TAG, "keyguard gone, waking up from dream");
+ mPM.wakeUp(mSystemClock.uptimeMillis(),
+ mWakeAndUnlocking ? PowerManager.WAKE_REASON_BIOMETRIC
+ : PowerManager.WAKE_REASON_GESTURE,
+ "com.android.systemui:UNLOCK_DREAMING");
+ }
+ }
Trace.endSection();
}
@@ -1322,6 +1346,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
KeyguardTransitions keyguardTransitions,
InteractionJankMonitor interactionJankMonitor,
DreamOverlayStateController dreamOverlayStateController,
+ JavaAdapter javaAdapter,
+ WallpaperRepository wallpaperRepository,
Lazy<ShadeController> shadeControllerLazy,
Lazy<NotificationShadeWindowController> notificationShadeWindowControllerLazy,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
@@ -1382,6 +1408,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mScreenOffAnimationController = screenOffAnimationController;
mInteractionJankMonitor = interactionJankMonitor;
mDreamOverlayStateController = dreamOverlayStateController;
+ mJavaAdapter = javaAdapter;
+ mWallpaperRepository = wallpaperRepository;
mActivityLaunchAnimator = activityLaunchAnimator;
mScrimControllerLazy = scrimControllerLazy;
@@ -1484,6 +1512,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
com.android.internal.R.anim.lock_screen_behind_enter);
mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
+
+ mJavaAdapter.alwaysCollectFlow(
+ mWallpaperRepository.getWallpaperSupportsAmbientMode(),
+ this::setWallpaperSupportsAmbientMode);
}
// TODO(b/273443374) remove, temporary util to get a feature flag
@@ -2653,6 +2685,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mKeyguardExitAnimationRunner = null;
mWakeAndUnlocking = false;
+ mUnlockingAndWakingFromDream = false;
setPendingLock(false);
// Force if we we're showing in the middle of hiding, to ensure we end up in the correct
@@ -2777,7 +2810,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mHiding = true;
- if (mShowing && !mOccluded) {
+ mUnlockingAndWakingFromDream = mStatusBarStateController.isDreaming()
+ && !mStatusBarStateController.isDozing();
+
+ if ((mShowing && !mOccluded) || mUnlockingAndWakingFromDream) {
+ if (mUnlockingAndWakingFromDream) {
+ Log.d(TAG, "hiding keyguard before waking from dream");
+ }
mKeyguardGoingAwayRunnable.run();
} else {
// TODO(bc-unlock): Fill parameters
@@ -2788,13 +2827,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
null /* nonApps */, null /* finishedCallback */);
});
}
-
- // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
- // dreaming. It's time to wake up.
- if (mDreamOverlayShowing || mUpdateMonitor.isDreaming()) {
- mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
- "com.android.systemui:UNLOCK_DREAMING");
- }
}
Trace.endSection();
}
@@ -2964,6 +2996,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
private void onKeyguardExitFinished() {
+ if (DEBUG) Log.d(TAG, "onKeyguardExitFinished()");
// only play "unlock" noises if not on a call (since the incall UI
// disables the keyguard)
if (TelephonyManager.EXTRA_STATE_IDLE.equals(mPhoneState)) {
@@ -3185,13 +3218,14 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
flags |= StatusBarManager.DISABLE_RECENT;
}
- if (mPowerGestureIntercepted) {
+ if (mPowerGestureIntercepted && mOccluded && isSecure()) {
flags |= StatusBarManager.DISABLE_RECENT;
}
if (DEBUG) {
Log.d(TAG, "adjustStatusBarLocked: mShowing=" + mShowing + " mOccluded=" + mOccluded
+ " isSecure=" + isSecure() + " force=" + forceHideHomeRecentsButtons
+ + " mPowerGestureIntercepted=" + mPowerGestureIntercepted
+ " --> flags=0x" + Integer.toHexString(flags));
}
@@ -3419,6 +3453,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
pw.print(" mPendingLock: "); pw.println(mPendingLock);
pw.print(" wakeAndUnlocking: "); pw.println(mWakeAndUnlocking);
pw.print(" mPendingPinLock: "); pw.println(mPendingPinLock);
+ pw.print(" mPowerGestureIntercepted: "); pw.println(mPowerGestureIntercepted);
}
/**
@@ -3458,7 +3493,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
* In case it does support it, we have to fade in the incoming app, otherwise we'll reveal it
* with the light reveal scrim.
*/
- public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
+ private void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
mWallpaperSupportsAmbientMode = supportsAmbientMode;
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index 1978b3d048b7..039460d8fdae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -61,28 +61,23 @@ constructor(
override fun start() {
Log.d(LOG_TAG, "Resource trimmer registered.")
- if (
- !(featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK) ||
- featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK))
- ) {
- return
- }
-
- applicationScope.launch(bgDispatcher) {
- // We need to wait for the AoD transition (and animation) to complete.
- // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
- // signal. This is to make sure we don't clear font caches during animation which
- // would jank and leave stale data in memory.
- val isDozingFully =
- keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
- combine(
- keyguardInteractor.wakefulnessModel.map { it.state },
- keyguardInteractor.isDreaming,
- isDozingFully,
- ::Triple
- )
- .distinctUntilChanged()
- .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
+ if (featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) {
+ applicationScope.launch(bgDispatcher) {
+ // We need to wait for the AoD transition (and animation) to complete.
+ // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
+ // signal. This is to make sure we don't clear font caches during animation which
+ // would jank and leave stale data in memory.
+ val isDozingFully =
+ keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
+ combine(
+ keyguardInteractor.wakefulnessModel.map { it.state },
+ keyguardInteractor.isDreaming,
+ isDozingFully,
+ ::Triple
+ )
+ .distinctUntilChanged()
+ .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
+ }
}
applicationScope.launch(bgDispatcher) {
@@ -97,17 +92,16 @@ constructor(
@WorkerThread
private fun onKeyguardGone() {
- if (!featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
- return
- }
-
- if (DEBUG) {
- Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
- }
// We want to clear temporary caches we've created while rendering and animating
// lockscreen elements, especially clocks.
+ Log.d(LOG_TAG, "Sending TRIM_MEMORY_UI_HIDDEN.")
globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
- globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ if (featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
+ }
+ globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
+ }
}
@WorkerThread
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 29a2d12ca972..4205ed22ff24 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -66,9 +66,11 @@ import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.SystemClock;
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository;
import com.android.wm.shell.keyguard.KeyguardTransitions;
import dagger.Lazy;
@@ -130,6 +132,8 @@ public class KeyguardModule {
KeyguardTransitions keyguardTransitions,
InteractionJankMonitor interactionJankMonitor,
DreamOverlayStateController dreamOverlayStateController,
+ JavaAdapter javaAdapter,
+ WallpaperRepository wallpaperRepository,
Lazy<ShadeController> shadeController,
Lazy<NotificationShadeWindowController> notificationShadeWindowController,
Lazy<ActivityLaunchAnimator> activityLaunchAnimator,
@@ -170,6 +174,8 @@ public class KeyguardModule {
keyguardTransitions,
interactionJankMonitor,
dreamOverlayStateController,
+ javaAdapter,
+ wallpaperRepository,
shadeController,
notificationShadeWindowController,
activityLaunchAnimator,
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 0b6c7c415599..ff3e77c46f1c 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
@@ -325,6 +325,9 @@ constructor(
private class StrongAuthTracker(private val userRepository: UserRepository, context: Context?) :
LockPatternUtils.StrongAuthTracker(context) {
+ private val selectedUserId =
+ userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged()
+
// Backing field for onStrongAuthRequiredChanged
private val _authFlags =
MutableStateFlow(AuthenticationFlags(currentUserId, getStrongAuthForUser(currentUserId)))
@@ -336,15 +339,12 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont
)
val currentUserAuthFlags: Flow<AuthenticationFlags> =
- userRepository.selectedUserInfo
- .map { it.id }
- .distinctUntilChanged()
- .flatMapLatest { userId ->
- _authFlags
- .map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) }
- .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") }
- .onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) }
- }
+ selectedUserId.flatMapLatest { userId ->
+ _authFlags
+ .map { AuthenticationFlags(userId, getStrongAuthForUser(userId)) }
+ .onEach { Log.d(TAG, "currentUser authFlags changed, new value: $it") }
+ .onStart { emit(AuthenticationFlags(userId, getStrongAuthForUser(userId))) }
+ }
/** isStrongBiometricAllowed for the current user. */
val isStrongBiometricAllowed: Flow<Boolean> =
@@ -352,16 +352,17 @@ private class StrongAuthTracker(private val userRepository: UserRepository, cont
/** isNonStrongBiometricAllowed for the current user. */
val isNonStrongBiometricAllowed: Flow<Boolean> =
- userRepository.selectedUserInfo
- .map { it.id }
- .distinctUntilChanged()
+ selectedUserId
.flatMapLatest { userId ->
_nonStrongBiometricAllowed
.filter { it.first == userId }
.map { it.second }
- .onEach { Log.d(TAG, "isNonStrongBiometricAllowed changed for current user") }
+ .onEach {
+ Log.d(TAG, "isNonStrongBiometricAllowed changed for current user: $it")
+ }
.onStart { emit(isNonStrongBiometricAllowedAfterIdleTimeout(userId)) }
}
+ .and(isStrongBiometricAllowed)
private val currentUserId
get() = userRepository.getSelectedUserInfo().id
@@ -387,3 +388,6 @@ private fun DevicePolicyManager.isFingerprintDisabled(userId: Int): Boolean =
private fun DevicePolicyManager.isNotActive(userId: Int, policy: Int): Boolean =
(getKeyguardDisabledFeatures(null, userId) and policy) == 0
+
+private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>): Flow<Boolean> =
+ this.combine(anotherFlow) { a, b -> a && b }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index a3d1abedcc2c..6fd3e21b25a4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -26,6 +26,8 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.FaceAuthUiEvent
import com.android.systemui.Dumpable
import com.android.systemui.R
+import com.android.systemui.biometrics.data.repository.FacePropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
@@ -38,13 +40,13 @@ import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.AcquiredAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.FailedAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.AcquiredFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
+import com.android.systemui.keyguard.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.log.FaceAuthenticationLogger
import com.android.systemui.log.SessionTracker
@@ -58,6 +60,7 @@ import java.util.stream.Collectors
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
@@ -68,6 +71,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -88,10 +92,10 @@ interface DeviceEntryFaceAuthRepository {
val canRunFaceAuth: StateFlow<Boolean>
/** Provide the current status of face authentication. */
- val authenticationStatus: Flow<AuthenticationStatus>
+ val authenticationStatus: Flow<FaceAuthenticationStatus>
/** Provide the current status of face detection. */
- val detectionStatus: Flow<DetectionStatus>
+ val detectionStatus: Flow<FaceDetectionStatus>
/** Current state of whether face authentication is locked out or not. */
val isLockedOut: StateFlow<Boolean>
@@ -102,6 +106,9 @@ interface DeviceEntryFaceAuthRepository {
/** Whether bypass is currently enabled */
val isBypassEnabled: Flow<Boolean>
+ /** Set whether face authentication should be locked out or not */
+ fun lockoutFaceAuth()
+
/**
* Trigger face authentication.
*
@@ -117,6 +124,7 @@ interface DeviceEntryFaceAuthRepository {
fun cancel()
}
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class DeviceEntryFaceAuthRepositoryImpl
@Inject
@@ -140,7 +148,8 @@ constructor(
@FaceDetectTableLog private val faceDetectLog: TableLogBuffer,
@FaceAuthTableLog private val faceAuthLog: TableLogBuffer,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
- private val featureFlags: FeatureFlags,
+ featureFlags: FeatureFlags,
+ facePropertyRepository: FacePropertyRepository,
dumpManager: DumpManager,
) : DeviceEntryFaceAuthRepository, Dumpable {
private var authCancellationSignal: CancellationSignal? = null
@@ -151,15 +160,22 @@ constructor(
private var cancelNotReceivedHandlerJob: Job? = null
private var halErrorRetryJob: Job? = null
- private val _authenticationStatus: MutableStateFlow<AuthenticationStatus?> =
+ private val _authenticationStatus: MutableStateFlow<FaceAuthenticationStatus?> =
MutableStateFlow(null)
- override val authenticationStatus: Flow<AuthenticationStatus>
+ override val authenticationStatus: Flow<FaceAuthenticationStatus>
get() = _authenticationStatus.filterNotNull()
- private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
- override val detectionStatus: Flow<DetectionStatus>
+ private val _detectionStatus = MutableStateFlow<FaceDetectionStatus?>(null)
+ override val detectionStatus: Flow<FaceDetectionStatus>
get() = _detectionStatus.filterNotNull()
+ private val isFaceBiometricsAllowed: Flow<Boolean> =
+ facePropertyRepository.sensorInfo.flatMapLatest {
+ if (it?.strength == SensorStrength.STRONG)
+ biometricSettingsRepository.isStrongBiometricAllowed
+ else biometricSettingsRepository.isNonStrongBiometricAllowed
+ }
+
private val _isLockedOut = MutableStateFlow(false)
override val isLockedOut: StateFlow<Boolean> = _isLockedOut
@@ -199,6 +215,10 @@ constructor(
}
?: flowOf(false)
+ override fun lockoutFaceAuth() {
+ _isLockedOut.value = true
+ }
+
private val faceLockoutResetCallback =
object : FaceManager.LockoutResetCallback() {
override fun onLockoutReset(sensorId: Int) {
@@ -267,10 +287,8 @@ constructor(
canFaceAuthOrDetectRun(faceDetectLog),
logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
logAndObserve(
- biometricSettingsRepository.isNonStrongBiometricAllowed
- .isFalse()
- .or(trustRepository.isCurrentUserTrusted),
- "nonStrongBiometricIsNotAllowedOrCurrentUserIsTrusted",
+ isFaceBiometricsAllowed.isFalse().or(trustRepository.isCurrentUserTrusted),
+ "biometricIsNotAllowedOrCurrentUserIsTrusted",
faceDetectLog
),
// We don't want to run face detect if fingerprint can be used to unlock the device
@@ -362,20 +380,11 @@ constructor(
canFaceAuthOrDetectRun(faceAuthLog),
logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog),
logAndObserve(
- deviceEntryFingerprintAuthRepository.isLockedOut.isFalse(),
- "fpIsNotLockedOut",
- faceAuthLog
- ),
- logAndObserve(
trustRepository.isCurrentUserTrusted.isFalse(),
"currentUserIsNotTrusted",
faceAuthLog
),
- logAndObserve(
- biometricSettingsRepository.isNonStrongBiometricAllowed,
- "nonStrongBiometricIsAllowed",
- faceAuthLog
- ),
+ logAndObserve(isFaceBiometricsAllowed, "isFaceBiometricsAllowed", faceAuthLog),
logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog),
)
.reduce(::and)
@@ -396,18 +405,18 @@ constructor(
private val faceAuthCallback =
object : FaceManager.AuthenticationCallback() {
override fun onAuthenticationFailed() {
- _authenticationStatus.value = FailedAuthenticationStatus
+ _authenticationStatus.value = FailedFaceAuthenticationStatus()
_isAuthenticated.value = false
faceAuthLogger.authenticationFailed()
onFaceAuthRequestCompleted()
}
override fun onAuthenticationAcquired(acquireInfo: Int) {
- _authenticationStatus.value = AcquiredAuthenticationStatus(acquireInfo)
+ _authenticationStatus.value = AcquiredFaceAuthenticationStatus(acquireInfo)
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {
- val errorStatus = ErrorAuthenticationStatus(errorCode, errString.toString())
+ val errorStatus = ErrorFaceAuthenticationStatus(errorCode, errString.toString())
if (errorStatus.isLockoutError()) {
_isLockedOut.value = true
}
@@ -433,11 +442,11 @@ constructor(
if (faceAcquiredInfoIgnoreList.contains(code)) {
return
}
- _authenticationStatus.value = HelpAuthenticationStatus(code, helpStr.toString())
+ _authenticationStatus.value = HelpFaceAuthenticationStatus(code, helpStr.toString())
}
override fun onAuthenticationSucceeded(result: FaceManager.AuthenticationResult) {
- _authenticationStatus.value = SuccessAuthenticationStatus(result)
+ _authenticationStatus.value = SuccessFaceAuthenticationStatus(result)
_isAuthenticated.value = true
faceAuthLogger.faceAuthSuccess(result)
onFaceAuthRequestCompleted()
@@ -482,7 +491,7 @@ constructor(
private val detectionCallback =
FaceManager.FaceDetectionCallback { sensorId, userId, isStrong ->
faceAuthLogger.faceDetected()
- _detectionStatus.value = DetectionStatus(sensorId, userId, isStrong)
+ _detectionStatus.value = FaceDetectionStatus(sensorId, userId, isStrong)
}
private var cancellationInProgress = false
@@ -545,11 +554,11 @@ constructor(
faceAuthLogger.detectionNotSupported(faceManager, faceManager?.sensorPropertiesInternal)
return
}
- if (_isAuthRunning.value || detectCancellationSignal != null) {
+ if (_isAuthRunning.value) {
faceAuthLogger.skippingDetection(_isAuthRunning.value, detectCancellationSignal != null)
return
}
-
+ detectCancellationSignal?.cancel()
detectCancellationSignal = CancellationSignal()
withContext(mainDispatcher) {
// We always want to invoke face detect in the main thread.
@@ -574,6 +583,7 @@ constructor(
if (authCancellationSignal == null) return
authCancellationSignal?.cancel()
+ cancelNotReceivedHandlerJob?.cancel()
cancelNotReceivedHandlerJob =
applicationScope.launch {
delay(DEFAULT_CANCEL_SIGNAL_TIMEOUT)
@@ -583,6 +593,7 @@ constructor(
cancellationInProgress,
faceAuthRequestedWhileCancellation
)
+ _authenticationStatus.value = ErrorFaceAuthenticationStatus.cancelNotReceivedError()
onFaceAuthRequestCompleted()
}
cancellationInProgress = true
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 52234b32b83a..9bec30052476 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
@@ -21,27 +21,29 @@ import android.hardware.biometrics.BiometricAuthenticator.Modality
import android.hardware.biometrics.BiometricSourceType
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
-import com.android.systemui.Dumpable
import com.android.systemui.biometrics.AuthController
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dump.DumpManager
-import java.io.PrintWriter
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
/** Encapsulates state about device entry fingerprint auth mechanism. */
interface DeviceEntryFingerprintAuthRepository {
/** Whether the device entry fingerprint auth is locked out. */
- val isLockedOut: StateFlow<Boolean>
+ val isLockedOut: Flow<Boolean>
/**
* Whether the fingerprint sensor is currently listening, this doesn't mean that the user is
@@ -53,6 +55,9 @@ interface DeviceEntryFingerprintAuthRepository {
* Fingerprint sensor type present on the device, null if fingerprint sensor is not available.
*/
val availableFpSensorType: Flow<BiometricType?>
+
+ /** Provide the current status of fingerprint authentication. */
+ val authenticationStatus: Flow<FingerprintAuthenticationStatus>
}
/**
@@ -69,16 +74,7 @@ constructor(
val authController: AuthController,
val keyguardUpdateMonitor: KeyguardUpdateMonitor,
@Application scope: CoroutineScope,
- dumpManager: DumpManager,
-) : DeviceEntryFingerprintAuthRepository, Dumpable {
-
- init {
- dumpManager.registerDumpable(this)
- }
-
- override fun dump(pw: PrintWriter, args: Array<String?>) {
- pw.println("isLockedOut=${isLockedOut.value}")
- }
+) : DeviceEntryFingerprintAuthRepository {
override val availableFpSensorType: Flow<BiometricType?>
get() {
@@ -114,7 +110,7 @@ constructor(
else if (authController.isRearFpsSupported) BiometricType.REAR_FINGERPRINT else null
}
- override val isLockedOut: StateFlow<Boolean> =
+ override val isLockedOut: Flow<Boolean> =
conflatedCallbackFlow {
val sendLockoutUpdate =
fun() {
@@ -138,7 +134,7 @@ constructor(
sendLockoutUpdate()
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
- .stateIn(scope, started = SharingStarted.Eagerly, initialValue = false)
+ .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
override val isRunning: Flow<Boolean>
get() = conflatedCallbackFlow {
@@ -166,6 +162,93 @@ constructor(
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
+ override val authenticationStatus: Flow<FingerprintAuthenticationStatus>
+ get() = conflatedCallbackFlow {
+ val callback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onBiometricAuthenticated(
+ userId: Int,
+ biometricSourceType: BiometricSourceType,
+ isStrongBiometric: Boolean,
+ ) {
+
+ sendUpdateIfFingerprint(
+ biometricSourceType,
+ SuccessFingerprintAuthenticationStatus(
+ userId,
+ isStrongBiometric,
+ ),
+ )
+ }
+
+ override fun onBiometricError(
+ msgId: Int,
+ errString: String?,
+ biometricSourceType: BiometricSourceType,
+ ) {
+ sendUpdateIfFingerprint(
+ biometricSourceType,
+ ErrorFingerprintAuthenticationStatus(
+ msgId,
+ errString,
+ ),
+ )
+ }
+
+ override fun onBiometricHelp(
+ msgId: Int,
+ helpString: String?,
+ biometricSourceType: BiometricSourceType,
+ ) {
+ sendUpdateIfFingerprint(
+ biometricSourceType,
+ HelpFingerprintAuthenticationStatus(
+ msgId,
+ helpString,
+ ),
+ )
+ }
+
+ override fun onBiometricAuthFailed(
+ biometricSourceType: BiometricSourceType,
+ ) {
+ sendUpdateIfFingerprint(
+ biometricSourceType,
+ FailFingerprintAuthenticationStatus,
+ )
+ }
+
+ override fun onBiometricAcquired(
+ biometricSourceType: BiometricSourceType,
+ acquireInfo: Int,
+ ) {
+ sendUpdateIfFingerprint(
+ biometricSourceType,
+ AcquiredFingerprintAuthenticationStatus(
+ acquireInfo,
+ ),
+ )
+ }
+
+ private fun sendUpdateIfFingerprint(
+ biometricSourceType: BiometricSourceType,
+ authenticationStatus: FingerprintAuthenticationStatus
+ ) {
+ if (biometricSourceType != BiometricSourceType.FINGERPRINT) {
+ return
+ }
+
+ trySendWithFailureLogging(
+ authenticationStatus,
+ TAG,
+ "new fingerprint authentication status"
+ )
+ }
+ }
+ keyguardUpdateMonitor.registerCallback(callback)
+ awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+ }
+
companion object {
const val TAG = "DeviceEntryFingerprintAuthRepositoryImpl"
}
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 42bee4a3bdcd..7475c4251211 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
@@ -31,11 +31,13 @@ import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
import com.android.systemui.dreams.DreamOverlayCallbackController
+import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.ScreenModel
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -49,9 +51,11 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
@@ -148,6 +152,9 @@ interface KeyguardRepository {
/** Observable for device wake/sleep state */
val wakefulness: StateFlow<WakefulnessModel>
+ /** Observable for device screen state */
+ val screenModel: StateFlow<ScreenModel>
+
/** Observable for biometric unlock modes */
val biometricUnlockState: Flow<BiometricUnlockModel>
@@ -163,6 +170,9 @@ interface KeyguardRepository {
/** Whether quick settings or quick-quick settings is visible. */
val isQuickSettingsVisible: Flow<Boolean>
+ /** Receive an event for doze time tick */
+ val dozeTimeTick: Flow<Unit>
+
/**
* Returns `true` if the keyguard is showing; `false` otherwise.
*
@@ -204,6 +214,8 @@ interface KeyguardRepository {
fun setIsDozing(isDozing: Boolean)
fun setIsActiveDreamLockscreenHosted(isLockscreenHosted: Boolean)
+
+ fun dozeTimeTick()
}
/** Encapsulates application state for the keyguard. */
@@ -213,6 +225,7 @@ class KeyguardRepositoryImpl
constructor(
statusBarStateController: StatusBarStateController,
wakefulnessLifecycle: WakefulnessLifecycle,
+ screenLifecycle: ScreenLifecycle,
biometricUnlockController: BiometricUnlockController,
private val keyguardStateController: KeyguardStateController,
private val keyguardBypassController: KeyguardBypassController,
@@ -370,6 +383,13 @@ constructor(
_isDozing.value = isDozing
}
+ private val _dozeTimeTick = MutableSharedFlow<Unit>()
+ override val dozeTimeTick = _dozeTimeTick.asSharedFlow()
+
+ override fun dozeTimeTick() {
+ _dozeTimeTick.tryEmit(Unit)
+ }
+
private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
@@ -559,6 +579,42 @@ constructor(
initialValue = WakefulnessModel.fromWakefulnessLifecycle(wakefulnessLifecycle),
)
+ override val screenModel: StateFlow<ScreenModel> =
+ conflatedCallbackFlow {
+ val observer =
+ object : ScreenLifecycle.Observer {
+ override fun onScreenTurningOn() {
+ dispatchNewState()
+ }
+ override fun onScreenTurnedOn() {
+ dispatchNewState()
+ }
+ override fun onScreenTurningOff() {
+ dispatchNewState()
+ }
+ override fun onScreenTurnedOff() {
+ dispatchNewState()
+ }
+
+ private fun dispatchNewState() {
+ trySendWithFailureLogging(
+ ScreenModel.fromScreenLifecycle(screenLifecycle),
+ TAG,
+ "updated screen state",
+ )
+ }
+ }
+
+ screenLifecycle.addObserver(observer)
+ awaitClose { screenLifecycle.removeObserver(observer) }
+ }
+ .stateIn(
+ scope,
+ // Use Eagerly so that we're always listening and never miss an event.
+ SharingStarted.Eagerly,
+ initialValue = ScreenModel.fromScreenLifecycle(screenLifecycle),
+ )
+
override val fingerprintSensorLocation: Flow<Point?> = conflatedCallbackFlow {
fun sendFpLocation() {
trySendWithFailureLogging(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
index 482e9a3d09d7..6a2511fdb90e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt
@@ -20,10 +20,13 @@ package com.android.systemui.keyguard.data.repository
import android.content.Context
import android.graphics.Point
+import androidx.core.animation.Animator
+import androidx.core.animation.ValueAnimator
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LiftReveal
import com.android.systemui.statusbar.LightRevealEffect
@@ -31,9 +34,12 @@ import com.android.systemui.statusbar.PowerButtonReveal
import javax.inject.Inject
import kotlin.math.max
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -52,6 +58,10 @@ interface LightRevealScrimRepository {
* at the current screen position of the appropriate sensor.
*/
val revealEffect: Flow<LightRevealEffect>
+
+ val revealAmount: Flow<Float>
+
+ fun startRevealAmountAnimator(reveal: Boolean)
}
@SysUISingleton
@@ -108,13 +118,30 @@ constructor(
/** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */
private val nonBiometricRevealEffect: Flow<LightRevealEffect?> =
- keyguardRepository.wakefulness.flatMapLatest { wakefulnessModel ->
- when {
- wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
- wakefulnessModel.isAwakeFromTap() -> tapRevealEffect
- else -> flowOf(LiftReveal)
+ keyguardRepository.wakefulness
+ .filter { it.isStartingToWake() || it.isStartingToSleep() }
+ .flatMapLatest { wakefulnessModel ->
+ when {
+ wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect
+ wakefulnessModel.isWakingFrom(TAP) -> tapRevealEffect
+ else -> flowOf(LiftReveal)
+ }
}
- }
+
+ private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 500 }
+
+ override val revealAmount: Flow<Float> = callbackFlow {
+ val updateListener =
+ Animator.AnimatorUpdateListener {
+ trySend((it as ValueAnimator).animatedValue as Float)
+ }
+ revealAmountAnimator.addUpdateListener(updateListener)
+ awaitClose { revealAmountAnimator.removeUpdateListener(updateListener) }
+ }
+
+ override fun startRevealAmountAnimator(reveal: Boolean) {
+ if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse()
+ }
override val revealEffect =
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
index abe59b76816f..5ef9a9e0482c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt
@@ -18,8 +18,8 @@ package com.android.systemui.keyguard.data.repository
import com.android.keyguard.FaceAuthUiEvent
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -40,10 +40,10 @@ class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceA
private val _canRunFaceAuth = MutableStateFlow(false)
override val canRunFaceAuth: StateFlow<Boolean> = _canRunFaceAuth
- override val authenticationStatus: Flow<AuthenticationStatus>
+ override val authenticationStatus: Flow<FaceAuthenticationStatus>
get() = emptyFlow()
- override val detectionStatus: Flow<DetectionStatus>
+ override val detectionStatus: Flow<FaceDetectionStatus>
get() = emptyFlow()
private val _isLockedOut = MutableStateFlow(false)
@@ -55,6 +55,8 @@ class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceA
override val isBypassEnabled: Flow<Boolean>
get() = emptyFlow()
+ override fun lockoutFaceAuth() = Unit
+
/**
* Trigger face authentication.
*
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
new file mode 100644
index 000000000000..c849b8495a26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractor.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.res.Resources
+import android.hardware.biometrics.BiometricSourceType
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED
+import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.util.IndicationHelper
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+
+/**
+ * BiometricMessage business logic. Filters biometric error/acquired/fail/success events for
+ * authentication events that should never surface a message to the user at the current device
+ * state.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class BiometricMessageInteractor
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val fingerprintPropertyRepository: FingerprintPropertyRepository,
+ private val indicationHelper: IndicationHelper,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+ val fingerprintErrorMessage: Flow<BiometricMessage> =
+ fingerprintAuthRepository.authenticationStatus
+ .filter {
+ it is ErrorFingerprintAuthenticationStatus &&
+ !indicationHelper.shouldSuppressErrorMsg(FINGERPRINT, it.msgId)
+ }
+ .map {
+ val errorStatus = it as ErrorFingerprintAuthenticationStatus
+ BiometricMessage(
+ FINGERPRINT,
+ BiometricMessageType.ERROR,
+ errorStatus.msgId,
+ errorStatus.msg,
+ )
+ }
+
+ val fingerprintHelpMessage: Flow<BiometricMessage> =
+ fingerprintAuthRepository.authenticationStatus
+ .filter { it is HelpFingerprintAuthenticationStatus }
+ .filterNot { isPrimaryAuthRequired() }
+ .map {
+ val helpStatus = it as HelpFingerprintAuthenticationStatus
+ BiometricMessage(
+ FINGERPRINT,
+ BiometricMessageType.HELP,
+ helpStatus.msgId,
+ helpStatus.msg,
+ )
+ }
+
+ val fingerprintFailMessage: Flow<BiometricMessage> =
+ isUdfps().flatMapLatest { isUdfps ->
+ fingerprintAuthRepository.authenticationStatus
+ .filter { it is FailFingerprintAuthenticationStatus }
+ .filterNot { isPrimaryAuthRequired() }
+ .map {
+ BiometricMessage(
+ FINGERPRINT,
+ BiometricMessageType.FAIL,
+ BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+ if (isUdfps) {
+ resources.getString(
+ com.android.internal.R.string.fingerprint_udfps_error_not_match
+ )
+ } else {
+ resources.getString(
+ com.android.internal.R.string.fingerprint_error_not_match
+ )
+ },
+ )
+ }
+ }
+
+ private fun isUdfps() =
+ fingerprintPropertyRepository.sensorType.map {
+ it == FingerprintSensorType.UDFPS_OPTICAL ||
+ it == FingerprintSensorType.UDFPS_ULTRASONIC
+ }
+
+ private fun isPrimaryAuthRequired(): Boolean {
+ // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong
+ // as long as primary auth, i.e. PIN/pattern/password, is required), so it's ok to
+ // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the
+ // check of whether non-strong biometric is allowed since strong biometrics can still be
+ // used.
+ return !keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
+ }
+}
+
+data class BiometricMessage(
+ val source: BiometricSourceType,
+ val type: BiometricMessageType,
+ val id: Int,
+ val message: String?,
+)
+
+enum class BiometricMessageType {
+ HELP,
+ ERROR,
+ FAIL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt
index 2efcd0c1ffe7..0c898befe6a0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DozeInteractor.kt
@@ -35,4 +35,8 @@ constructor(
fun setLastTapToWakePosition(position: Point) {
keyguardRepository.setLastDozeTapToWakePosition(position)
}
+
+ fun dozeTimeTick() {
+ keyguardRepository.dozeTimeTick()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
index 74ef7a50fd44..e57c919a5b3e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt
@@ -16,8 +16,8 @@
package com.android.systemui.keyguard.domain.interactor
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
import kotlinx.coroutines.flow.Flow
/**
@@ -27,10 +27,10 @@ import kotlinx.coroutines.flow.Flow
interface KeyguardFaceAuthInteractor {
/** Current authentication status */
- val authenticationStatus: Flow<AuthenticationStatus>
+ val authenticationStatus: Flow<FaceAuthenticationStatus>
/** Current detection status */
- val detectionStatus: Flow<DetectionStatus>
+ val detectionStatus: Flow<FaceDetectionStatus>
/** Can face auth be run right now */
fun canFaceAuthRun(): Boolean
@@ -60,6 +60,7 @@ interface KeyguardFaceAuthInteractor {
fun onNotificationPanelClicked()
fun onSwipeUpOnBouncer()
fun onPrimaryBouncerUserInput()
+ fun onAccessibilityAction()
}
/**
@@ -72,8 +73,8 @@ interface KeyguardFaceAuthInteractor {
*/
interface FaceAuthenticationListener {
/** Receive face authentication status updates */
- fun onAuthenticationStatusChanged(status: AuthenticationStatus)
+ fun onAuthenticationStatusChanged(status: FaceAuthenticationStatus)
/** Receive status updates whenever face detection runs */
- fun onDetectionStatusChanged(status: DetectionStatus)
+ fun onDetectionStatusChanged(status: FaceDetectionStatus)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 7fae7522d981..1553525915d5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -70,6 +70,8 @@ constructor(
val dozeAmount: Flow<Float> = repository.linearDozeAmount
/** Whether the system is in doze mode. */
val isDozing: Flow<Boolean> = repository.isDozing
+ /** Receive an event for doze time tick */
+ val dozeTimeTick: Flow<Unit> = repository.dozeTimeTick
/** Whether Always-on Display mode is available. */
val isAodAvailable: Flow<Boolean> = repository.isAodAvailable
/** Doze transition information. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 324d443d974d..40e0604ae1b3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -409,6 +409,10 @@ constructor(
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_TRANSIT_CLOCK,
value = featureFlags.isEnabled(Flags.TRANSIT_CLOCK)
+ ),
+ KeyguardPickerFlag(
+ name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS,
+ value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS)
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 833eda77108d..4244e5565a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -17,28 +17,44 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
@ExperimentalCoroutinesApi
@SysUISingleton
class LightRevealScrimInteractor
@Inject
constructor(
- transitionRepository: KeyguardTransitionRepository,
- transitionInteractor: KeyguardTransitionInteractor,
- lightRevealScrimRepository: LightRevealScrimRepository,
+ private val transitionInteractor: KeyguardTransitionInteractor,
+ private val lightRevealScrimRepository: LightRevealScrimRepository,
+ @Application private val scope: CoroutineScope,
) {
+ init {
+ listenForStartedKeyguardTransitionStep()
+ }
+
+ private fun listenForStartedKeyguardTransitionStep() {
+ scope.launch {
+ transitionInteractor.startedKeyguardTransitionStep.collect {
+ if (willTransitionChangeEndState(it)) {
+ lightRevealScrimRepository.startRevealAmountAnimator(
+ willBeRevealedInState(it.to)
+ )
+ }
+ }
+ }
+ }
+
/**
* Whenever a keyguard transition starts, sample the latest reveal effect from the repository
* and use that for the starting transition.
@@ -54,17 +70,7 @@ constructor(
lightRevealScrimRepository.revealEffect
)
- /**
- * The reveal amount to use for the light reveal scrim, which is derived from the keyguard
- * transition steps.
- */
- val revealAmount: Flow<Float> =
- transitionRepository.transitions
- // Only listen to transitions that change the reveal amount.
- .filter { willTransitionAffectRevealAmount(it) }
- // Use the transition amount as the reveal amount, inverting it if we're transitioning
- // to a non-revealed (hidden) state.
- .map { step -> if (willBeRevealedInState(step.to)) step.value else 1f - step.value }
+ val revealAmount = lightRevealScrimRepository.revealAmount
companion object {
@@ -72,7 +78,7 @@ constructor(
* Whether the transition requires a change in the reveal amount of the light reveal scrim.
* If not, we don't care about the transition and don't need to listen to it.
*/
- fun willTransitionAffectRevealAmount(transition: TransitionStep): Boolean {
+ fun willTransitionChangeEndState(transition: TransitionStep): Boolean {
return willBeRevealedInState(transition.from) != willBeRevealedInState(transition.to)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
index 5005b6c7f0df..596a1c01ca42 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt
@@ -17,8 +17,8 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
@@ -31,9 +31,9 @@ import kotlinx.coroutines.flow.emptyFlow
*/
@SysUISingleton
class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInteractor {
- override val authenticationStatus: Flow<AuthenticationStatus>
+ override val authenticationStatus: Flow<FaceAuthenticationStatus>
get() = emptyFlow()
- override val detectionStatus: Flow<DetectionStatus>
+ override val detectionStatus: Flow<FaceDetectionStatus>
get() = emptyFlow()
override fun canFaceAuthRun(): Boolean = false
@@ -60,4 +60,5 @@ class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInt
override fun onSwipeUpOnBouncer() {}
override fun onPrimaryBouncerUserInput() {}
+ override fun onAccessibilityAction() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt
new file mode 100644
index 000000000000..a2287c756e5b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractor.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintManager
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.plugins.ActivityStarter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.launch
+
+/** Business logic for handling authentication events when an app is occluding the lockscreen. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class OccludingAppDeviceEntryInteractor
+@Inject
+constructor(
+ biometricMessageInteractor: BiometricMessageInteractor,
+ fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ keyguardInteractor: KeyguardInteractor,
+ primaryBouncerInteractor: PrimaryBouncerInteractor,
+ alternateBouncerInteractor: AlternateBouncerInteractor,
+ @Application scope: CoroutineScope,
+ private val context: Context,
+ activityStarter: ActivityStarter,
+) {
+ private val keyguardOccludedByApp: Flow<Boolean> =
+ combine(
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardInteractor.isKeyguardShowing,
+ primaryBouncerInteractor.isShowing,
+ alternateBouncerInteractor.isVisible,
+ ) { occluded, showing, primaryBouncerShowing, alternateBouncerVisible ->
+ occluded && showing && !primaryBouncerShowing && !alternateBouncerVisible
+ }
+ .distinctUntilChanged()
+ private val fingerprintUnlockSuccessEvents: Flow<Unit> =
+ fingerprintAuthRepository.authenticationStatus
+ .ifKeyguardOccludedByApp()
+ .filter { it is SuccessFingerprintAuthenticationStatus }
+ .map {} // maps FingerprintAuthenticationStatus => Unit
+ private val fingerprintLockoutEvents: Flow<Unit> =
+ fingerprintAuthRepository.authenticationStatus
+ .ifKeyguardOccludedByApp()
+ .filter {
+ it is ErrorFingerprintAuthenticationStatus &&
+ (it.msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT ||
+ it.msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
+ }
+ .map {} // maps FingerprintAuthenticationStatus => Unit
+ val message: Flow<BiometricMessage?> =
+ merge(
+ biometricMessageInteractor.fingerprintErrorMessage,
+ biometricMessageInteractor.fingerprintFailMessage,
+ biometricMessageInteractor.fingerprintHelpMessage,
+ )
+ .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null))
+
+ init {
+ scope.launch {
+ // On fingerprint success, go to the home screen
+ fingerprintUnlockSuccessEvents.collect { goToHomeScreen() }
+ }
+
+ scope.launch {
+ // On device fingerprint lockout, request the bouncer with a runnable to
+ // go to the home screen. Without this, the bouncer won't proceed to the home screen.
+ fingerprintLockoutEvents.collect {
+ activityStarter.dismissKeyguardThenExecute(
+ object : ActivityStarter.OnDismissAction {
+ override fun onDismiss(): Boolean {
+ goToHomeScreen()
+ return false
+ }
+
+ override fun willRunAnimationOnKeyguard(): Boolean {
+ return false
+ }
+ },
+ /* cancel= */ null,
+ /* afterKeyguardGone */ false
+ )
+ }
+ }
+ }
+
+ /** Launches an Activity which forces the current app to background by going home. */
+ private fun goToHomeScreen() {
+ context.startActivity(
+ Intent(Intent.ACTION_MAIN).apply {
+ addCategory(Intent.CATEGORY_HOME)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ )
+ }
+
+ private fun <T> Flow<T>.ifKeyguardOccludedByApp(elseFlow: Flow<T> = emptyFlow()): Flow<T> {
+ return keyguardOccludedByApp.flatMapLatest { keyguardOccludedByApp ->
+ if (keyguardOccludedByApp) {
+ this
+ } else {
+ elseFlow
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
index d467225a9d63..8f4776fa2ed3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt
@@ -30,8 +30,9 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.log.FaceAuthenticationLogger
import com.android.systemui.util.kotlin.pairwise
@@ -67,6 +68,7 @@ constructor(
private val featureFlags: FeatureFlags,
private val faceAuthenticationLogger: FaceAuthenticationLogger,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
) : CoreStartable, KeyguardFaceAuthInteractor {
private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf()
@@ -117,6 +119,15 @@ constructor(
)
}
.launchIn(applicationScope)
+
+ deviceEntryFingerprintAuthRepository.isLockedOut
+ .onEach {
+ if (it) {
+ faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
+ repository.lockoutFaceAuth()
+ }
+ }
+ .launchIn(applicationScope)
}
override fun onSwipeUpOnBouncer() {
@@ -143,6 +154,10 @@ constructor(
runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false)
}
+ override fun onAccessibilityAction() {
+ runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false)
+ }
+
override fun registerListener(listener: FaceAuthenticationListener) {
listeners.add(listener)
}
@@ -165,10 +180,10 @@ constructor(
repository.cancel()
}
- private val _authenticationStatusOverride = MutableStateFlow<AuthenticationStatus?>(null)
+ private val faceAuthenticationStatusOverride = MutableStateFlow<FaceAuthenticationStatus?>(null)
/** Provide the status of face authentication */
override val authenticationStatus =
- merge(_authenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
+ merge(faceAuthenticationStatusOverride.filterNotNull(), repository.authenticationStatus)
/** Provide the status of face detection */
override val detectionStatus = repository.detectionStatus
@@ -176,13 +191,13 @@ constructor(
private fun runFaceAuth(uiEvent: FaceAuthUiEvent, fallbackToDetect: Boolean) {
if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
if (repository.isLockedOut.value) {
- _authenticationStatusOverride.value =
- ErrorAuthenticationStatus(
+ faceAuthenticationStatusOverride.value =
+ ErrorFaceAuthenticationStatus(
BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT,
context.resources.getString(R.string.keyguard_face_unlock_unavailable)
)
} else {
- _authenticationStatusOverride.value = null
+ faceAuthenticationStatusOverride.value = null
applicationScope.launch {
faceAuthenticationLogger.authRequested(uiEvent)
repository.authenticate(uiEvent, fallbackToDetection = fallbackToDetect)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
index bba0e37d8ed0..c0308e6c5759 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractor.kt
@@ -21,10 +21,16 @@ import android.animation.FloatEvaluator
import android.animation.IntEvaluator
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.statusbar.phone.hideAffordancesRequest
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
/** Encapsulates business logic for transitions between UDFPS states on the keyguard. */
@ExperimentalCoroutinesApi
@@ -35,6 +41,8 @@ constructor(
configRepo: ConfigurationRepository,
burnInInteractor: BurnInInteractor,
keyguardInteractor: KeyguardInteractor,
+ shadeRepository: ShadeRepository,
+ dialogManager: SystemUIDialogManager,
) {
private val intEvaluator = IntEvaluator()
private val floatEvaluator = FloatEvaluator()
@@ -56,6 +64,26 @@ constructor(
floatEvaluator.evaluate(dozeAmount, 0, fullyDozingBurnInProgress),
)
}
+
+ val dialogHideAffordancesRequest: Flow<Boolean> = dialogManager.hideAffordancesRequest
+
+ val qsProgress: Flow<Float> =
+ shadeRepository.qsExpansion // swipe from top of LS
+ .map { (it * 2).coerceIn(0f, 1f) }
+ .onStart { emit(0f) }
+
+ val shadeExpansion: Flow<Float> =
+ combine(
+ shadeRepository.udfpsTransitionToFullShadeProgress, // swipe from middle of LS
+ keyguardInteractor.statusBarState, // quick swipe from middle of LS
+ ) { shadeProgress, statusBarState ->
+ if (statusBarState == StatusBarState.SHADE_LOCKED) {
+ 1f
+ } else {
+ shadeProgress
+ }
+ }
+ .onStart { emit(0f) }
}
data class BurnInOffsets(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index b354cfd27687..3de3666fdc3c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -23,33 +23,47 @@ import android.os.SystemClock.elapsedRealtime
* Authentication status provided by
* [com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository]
*/
-sealed class AuthenticationStatus
+sealed class FaceAuthenticationStatus
/** Success authentication status. */
-data class SuccessAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) :
- AuthenticationStatus()
+data class SuccessFaceAuthenticationStatus(
+ val successResult: FaceManager.AuthenticationResult,
+ // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face authentication help message. */
-data class HelpAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus()
+data class HelpFaceAuthenticationStatus(
+ val msgId: Int,
+ val msg: String?, // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face acquired message. */
-data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationStatus()
+data class AcquiredFaceAuthenticationStatus(
+ val acquiredInfo: Int, // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face authentication failed message. */
-object FailedAuthenticationStatus : AuthenticationStatus()
+data class FailedFaceAuthenticationStatus(
+ // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus()
/** Face authentication error message */
-data class ErrorAuthenticationStatus(
+data class ErrorFaceAuthenticationStatus(
val msgId: Int,
val msg: String? = null,
// present to break equality check if the same error occurs repeatedly.
- val createdAt: Long = elapsedRealtime()
-) : AuthenticationStatus() {
+ @JvmField val createdAt: Long = elapsedRealtime()
+) : FaceAuthenticationStatus() {
/**
* Method that checks if [msgId] is a lockout error. A lockout error means that face
* authentication is locked out.
*/
- fun isLockoutError() = msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT
+ fun isLockoutError() =
+ msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT || msgId == FaceManager.FACE_ERROR_LOCKOUT
/**
* Method that checks if [msgId] is a cancellation error. This means that face authentication
@@ -61,7 +75,21 @@ data class ErrorAuthenticationStatus(
fun isHardwareError() =
msgId == FaceManager.FACE_ERROR_HW_UNAVAILABLE ||
msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS
+
+ companion object {
+ /**
+ * Error message that is created when cancel confirmation is not received from FaceManager
+ * after we request for a cancellation of face auth.
+ */
+ fun cancelNotReceivedError() = ErrorFaceAuthenticationStatus(-1, "")
+ }
}
/** Face detection success message. */
-data class DetectionStatus(val sensorId: Int, val userId: Int, val isStrongBiometric: Boolean)
+data class FaceDetectionStatus(
+ val sensorId: Int,
+ val userId: Int,
+ val isStrongBiometric: Boolean,
+ // present to break equality check if the same error occurs repeatedly.
+ @JvmField val createdAt: Long = elapsedRealtime()
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
new file mode 100644
index 000000000000..7fc6016bf087
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+import android.os.SystemClock.elapsedRealtime
+
+/**
+ * Fingerprint authentication status provided by
+ * [com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository]
+ */
+sealed class FingerprintAuthenticationStatus
+
+/** Fingerprint authentication success status. */
+data class SuccessFingerprintAuthenticationStatus(
+ val userId: Int,
+ val isStrongBiometric: Boolean,
+) : FingerprintAuthenticationStatus()
+
+/** Fingerprint authentication help message. */
+data class HelpFingerprintAuthenticationStatus(
+ val msgId: Int,
+ val msg: String?,
+) : FingerprintAuthenticationStatus()
+
+/** Fingerprint acquired message. */
+data class AcquiredFingerprintAuthenticationStatus(val acquiredInfo: Int) :
+ FingerprintAuthenticationStatus()
+
+/** Fingerprint authentication failed message. */
+object FailFingerprintAuthenticationStatus : FingerprintAuthenticationStatus()
+
+/** Fingerprint authentication error message */
+data class ErrorFingerprintAuthenticationStatus(
+ val msgId: Int,
+ val msg: String? = null,
+ // present to break equality check if the same error occurs repeatedly.
+ val createdAt: Long = elapsedRealtime(),
+) : FingerprintAuthenticationStatus()
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenModel.kt
index c48028c31cf0..80a1b75c4350 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenModel.kt
@@ -11,18 +11,19 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
- *
+ * limitations under the License
*/
+package com.android.systemui.keyguard.shared.model
-package com.android.systemui.multishade.data.model
-
-import com.android.systemui.multishade.shared.model.ShadeId
+import com.android.systemui.keyguard.ScreenLifecycle
-/** Models the current interaction with one of the shades. */
-data class MultiShadeInteractionModel(
- /** The ID of the shade that the user is currently interacting with. */
- val shadeId: ShadeId,
- /** Whether the interaction is proxied (as in: coming from an external app or different UI). */
- val isProxied: Boolean,
-)
+/** Model device screen lifecycle states. */
+data class ScreenModel(
+ val state: ScreenState,
+) {
+ companion object {
+ fun fromScreenLifecycle(screenLifecycle: ScreenLifecycle): ScreenModel {
+ return ScreenModel(ScreenState.fromScreenLifecycleInt(screenLifecycle.getScreenState()))
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt
new file mode 100644
index 000000000000..fe5d9355a6fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/ScreenState.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+import com.android.systemui.keyguard.ScreenLifecycle
+
+enum class ScreenState {
+ /** Screen is fully off. */
+ SCREEN_OFF,
+ /** Signal that the screen is turning on. */
+ SCREEN_TURNING_ON,
+ /** Screen is fully on. */
+ SCREEN_ON,
+ /** Signal that the screen is turning off. */
+ SCREEN_TURNING_OFF;
+
+ companion object {
+ fun fromScreenLifecycleInt(value: Int): ScreenState {
+ return when (value) {
+ ScreenLifecycle.SCREEN_OFF -> SCREEN_OFF
+ ScreenLifecycle.SCREEN_TURNING_ON -> SCREEN_TURNING_ON
+ ScreenLifecycle.SCREEN_ON -> SCREEN_ON
+ ScreenLifecycle.SCREEN_TURNING_OFF -> SCREEN_TURNING_OFF
+ else -> throw IllegalArgumentException("Invalid screen value: $value")
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
index cfd9e0866c06..62f43ed82a96 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt
@@ -16,6 +16,13 @@
package com.android.systemui.keyguard.shared.model
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.GESTURE
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.POWER_BUTTON
+import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.ASLEEP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.AWAKE
+import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_SLEEP
+import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_WAKE
/** Model device wakefulness states. */
data class WakefulnessModel(
@@ -23,33 +30,31 @@ data class WakefulnessModel(
val lastWakeReason: WakeSleepReason,
val lastSleepReason: WakeSleepReason,
) {
- fun isStartingToWake() = state == WakefulnessState.STARTING_TO_WAKE
+ fun isStartingToWake() = state == STARTING_TO_WAKE
- fun isStartingToSleep() = state == WakefulnessState.STARTING_TO_SLEEP
+ fun isStartingToSleep() = state == STARTING_TO_SLEEP
- private fun isAsleep() = state == WakefulnessState.ASLEEP
+ private fun isAsleep() = state == ASLEEP
+
+ private fun isAwake() = state == AWAKE
+
+ fun isStartingToWakeOrAwake() = isStartingToWake() || isAwake()
fun isStartingToSleepOrAsleep() = isStartingToSleep() || isAsleep()
fun isDeviceInteractive() = !isAsleep()
- fun isStartingToWakeOrAwake() = isStartingToWake() || state == WakefulnessState.AWAKE
+ fun isWakingFrom(wakeSleepReason: WakeSleepReason) =
+ isStartingToWake() && lastWakeReason == wakeSleepReason
- fun isStartingToSleepFromPowerButton() =
- isStartingToSleep() && lastWakeReason == WakeSleepReason.POWER_BUTTON
-
- fun isWakingFromPowerButton() =
- isStartingToWake() && lastWakeReason == WakeSleepReason.POWER_BUTTON
+ fun isStartingToSleepFrom(wakeSleepReason: WakeSleepReason) =
+ isStartingToSleep() && lastSleepReason == wakeSleepReason
fun isTransitioningFromPowerButton() =
- isStartingToSleepFromPowerButton() || isWakingFromPowerButton()
-
- fun isAwakeFromTap() =
- state == WakefulnessState.STARTING_TO_WAKE && lastWakeReason == WakeSleepReason.TAP
+ isStartingToSleepFrom(POWER_BUTTON) || isWakingFrom(POWER_BUTTON)
fun isDeviceInteractiveFromTapOrGesture(): Boolean {
- return isDeviceInteractive() &&
- (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE)
+ return isDeviceInteractive() && (lastWakeReason == TAP || lastWakeReason == GESTURE)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
new file mode 100644
index 000000000000..1db596b346b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.annotation.DrawableRes
+import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.TintedIcon
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.temporarydisplay.ViewPriority
+import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+/** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */
+@ExperimentalCoroutinesApi
+object KeyguardRootViewBinder {
+ @JvmStatic
+ fun bind(
+ view: ViewGroup,
+ featureFlags: FeatureFlags,
+ occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
+ chipbarCoordinator: ChipbarCoordinator,
+ ) {
+ if (featureFlags.isEnabled(Flags.FP_LISTEN_OCCLUDING_APPS)) {
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch {
+ occludingAppDeviceEntryMessageViewModel.message.collect { biometricMessage
+ ->
+ if (biometricMessage?.message != null) {
+ chipbarCoordinator.displayView(
+ createChipbarInfo(
+ biometricMessage.message,
+ R.drawable.ic_lock,
+ )
+ )
+ } else {
+ chipbarCoordinator.removeView(ID, "occludingAppMsgNull")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates an instance of [ChipbarInfo] that can be sent to [ChipbarCoordinator] for display.
+ */
+ private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
+ return ChipbarInfo(
+ startIcon =
+ TintedIcon(
+ Icon.Resource(icon, null),
+ ChipbarInfo.DEFAULT_ICON_TINT,
+ ),
+ text = Text.Loaded(message),
+ endItem = null,
+ vibrationEffect = null,
+ windowTitle = "OccludingAppUnlockMsgChip",
+ wakeReason = "OCCLUDING_APP_UNLOCK_MSG_CHIP",
+ timeoutMs = 3500,
+ id = ID,
+ priority = ViewPriority.CRITICAL,
+ instanceId = null,
+ )
+ }
+
+ private const val ID = "occluding_app_device_entry_unlock_msg"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
index 728dd3911663..9872d97021fa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsAodFingerprintViewBinder.kt
@@ -37,6 +37,7 @@ object UdfpsAodFingerprintViewBinder {
view: LottieAnimationView,
viewModel: UdfpsAodViewModel,
) {
+ view.alpha = 0f
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
index 26ef4685d286..0113628c30ca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsBackgroundViewBinder.kt
@@ -38,6 +38,7 @@ object UdfpsBackgroundViewBinder {
view: ImageView,
viewModel: BackgroundViewModel,
) {
+ view.alpha = 0f
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
index 0ab8e52fb6c7..bab04f234b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/UdfpsFingerprintViewBinder.kt
@@ -42,6 +42,7 @@ object UdfpsFingerprintViewBinder {
view: LottieAnimationView,
viewModel: FingerprintViewModel,
) {
+ view.alpha = 0f
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index 389cf76c47ac..f1ceaaa391f5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -20,20 +20,18 @@ import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** View-model for the keyguard indication area view */
-@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardIndicationAreaViewModel
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
- private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
- private val keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
+ bottomAreaInteractor: KeyguardBottomAreaInteractor,
+ keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
private val burnInHelperWrapper: BurnInHelperWrapper,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
index a46d441613ac..82f40bf3a16a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt
@@ -19,12 +19,14 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.keyguard.domain.interactor.LightRevealScrimInteractor
import com.android.systemui.statusbar.LightRevealEffect
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
/**
* Models UI state for the light reveal scrim, which is used during screen on and off animations to
* draw a gradient that reveals/hides the contents of the screen.
*/
+@OptIn(ExperimentalCoroutinesApi::class)
class LightRevealScrimViewModel @Inject constructor(interactor: LightRevealScrimInteractor) {
val lightRevealEffect: Flow<LightRevealEffect> = interactor.lightRevealEffect
val revealAmount: Flow<Float> = interactor.revealAmount
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt
new file mode 100644
index 000000000000..3a162d7f14a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludingAppDeviceEntryMessageViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.BiometricMessage
+import com.android.systemui.keyguard.domain.interactor.OccludingAppDeviceEntryInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/** Shows authentication messages over occcluding apps over the lockscreen. */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class OccludingAppDeviceEntryMessageViewModel
+@Inject
+constructor(
+ interactor: OccludingAppDeviceEntryInteractor,
+) {
+ val message: Flow<BiometricMessage?> = interactor.message
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
index fd4b666a80fd..b307f1b43b45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModel.kt
@@ -21,13 +21,18 @@ import androidx.annotation.ColorInt
import com.android.settingslib.Utils.getColorAttrDefaultColor
import com.android.systemui.R
import com.android.systemui.keyguard.domain.interactor.BurnInOffsets
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.wm.shell.animation.Interpolators
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -38,6 +43,8 @@ open class UdfpsLockscreenViewModel(
lockscreenColorResId: Int,
alternateBouncerColorResId: Int,
transitionInteractor: KeyguardTransitionInteractor,
+ udfpsKeyguardInteractor: UdfpsKeyguardInteractor,
+ keyguardInteractor: KeyguardInteractor,
) {
private val toLockscreen: Flow<TransitionViewModel> =
transitionInteractor.anyStateToLockscreenTransition.map {
@@ -54,46 +61,53 @@ open class UdfpsLockscreenViewModel(
}
private val toAlternateBouncer: Flow<TransitionViewModel> =
- transitionInteractor.anyStateToAlternateBouncerTransition.map {
- TransitionViewModel(
- alpha = 1f,
- scale =
- if (visibleInKeyguardState(it.from)) {
- 1f
- } else {
- it.value
- },
- color = getColorAttrDefaultColor(context, alternateBouncerColorResId),
- )
- }
-
- private val fadeOut: Flow<TransitionViewModel> =
- merge(
- transitionInteractor.anyStateToGoneTransition,
- transitionInteractor.anyStateToAodTransition,
- transitionInteractor.anyStateToOccludedTransition,
- transitionInteractor.anyStateToPrimaryBouncerTransition,
- transitionInteractor.anyStateToDreamingTransition,
- )
- .map {
+ keyguardInteractor.statusBarState.flatMapLatest { statusBarState ->
+ transitionInteractor.anyStateToAlternateBouncerTransition.map {
TransitionViewModel(
- alpha =
- if (visibleInKeyguardState(it.from)) {
- 1f - it.value
- } else {
- 0f
- },
- scale = 1f,
- color =
- if (it.from == KeyguardState.ALTERNATE_BOUNCER) {
- getColorAttrDefaultColor(context, alternateBouncerColorResId)
+ alpha = 1f,
+ scale =
+ if (visibleInKeyguardState(it.from, statusBarState)) {
+ 1f
} else {
- getColorAttrDefaultColor(context, lockscreenColorResId)
+ Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it.value)
},
+ color = getColorAttrDefaultColor(context, alternateBouncerColorResId),
)
}
+ }
+
+ private val fadeOut: Flow<TransitionViewModel> =
+ keyguardInteractor.statusBarState.flatMapLatest { statusBarState ->
+ merge(
+ transitionInteractor.anyStateToGoneTransition,
+ transitionInteractor.anyStateToAodTransition,
+ transitionInteractor.anyStateToOccludedTransition,
+ transitionInteractor.anyStateToPrimaryBouncerTransition,
+ transitionInteractor.anyStateToDreamingTransition,
+ )
+ .map {
+ TransitionViewModel(
+ alpha =
+ if (visibleInKeyguardState(it.from, statusBarState)) {
+ 1f - it.value
+ } else {
+ 0f
+ },
+ scale = 1f,
+ color =
+ if (it.from == KeyguardState.ALTERNATE_BOUNCER) {
+ getColorAttrDefaultColor(context, alternateBouncerColorResId)
+ } else {
+ getColorAttrDefaultColor(context, lockscreenColorResId)
+ },
+ )
+ }
+ }
- private fun visibleInKeyguardState(state: KeyguardState): Boolean {
+ private fun visibleInKeyguardState(
+ state: KeyguardState,
+ statusBarState: StatusBarState
+ ): Boolean {
return when (state) {
KeyguardState.OFF,
KeyguardState.DOZING,
@@ -102,17 +116,53 @@ open class UdfpsLockscreenViewModel(
KeyguardState.PRIMARY_BOUNCER,
KeyguardState.GONE,
KeyguardState.OCCLUDED -> false
- KeyguardState.LOCKSCREEN,
+ KeyguardState.LOCKSCREEN -> statusBarState == StatusBarState.KEYGUARD
KeyguardState.ALTERNATE_BOUNCER -> true
}
}
- val transition: Flow<TransitionViewModel> =
+ private val keyguardStateTransition =
merge(
toAlternateBouncer,
toLockscreen,
fadeOut,
)
+
+ private val dialogHideAffordancesAlphaMultiplier: Flow<Float> =
+ udfpsKeyguardInteractor.dialogHideAffordancesRequest.map { hideAffordances ->
+ if (hideAffordances) {
+ 0f
+ } else {
+ 1f
+ }
+ }
+
+ private val alphaMultiplier: Flow<Float> =
+ combine(
+ transitionInteractor.startedKeyguardState,
+ dialogHideAffordancesAlphaMultiplier,
+ udfpsKeyguardInteractor.shadeExpansion,
+ udfpsKeyguardInteractor.qsProgress,
+ ) { startedKeyguardState, dialogHideAffordancesAlphaMultiplier, shadeExpansion, qsProgress
+ ->
+ if (startedKeyguardState == KeyguardState.ALTERNATE_BOUNCER) {
+ 1f
+ } else {
+ dialogHideAffordancesAlphaMultiplier * (1f - shadeExpansion) * (1f - qsProgress)
+ }
+ }
+
+ val transition: Flow<TransitionViewModel> =
+ combine(
+ alphaMultiplier,
+ keyguardStateTransition,
+ ) { alphaMultiplier, keyguardStateTransition ->
+ TransitionViewModel(
+ alpha = keyguardStateTransition.alpha * alphaMultiplier,
+ scale = keyguardStateTransition.scale,
+ color = keyguardStateTransition.color,
+ )
+ }
val visible: Flow<Boolean> = transition.map { it.alpha != 0f }
}
@@ -123,12 +173,15 @@ constructor(
val context: Context,
transitionInteractor: KeyguardTransitionInteractor,
interactor: UdfpsKeyguardInteractor,
+ keyguardInteractor: KeyguardInteractor,
) :
UdfpsLockscreenViewModel(
context,
android.R.attr.textColorPrimary,
com.android.internal.R.attr.materialColorOnPrimaryFixed,
transitionInteractor,
+ interactor,
+ keyguardInteractor,
) {
val dozeAmount: Flow<Float> = interactor.dozeAmount
val burnInOffsets: Flow<BurnInOffsets> = interactor.burnInOffsets
@@ -147,12 +200,16 @@ class BackgroundViewModel
constructor(
val context: Context,
transitionInteractor: KeyguardTransitionInteractor,
+ interactor: UdfpsKeyguardInteractor,
+ keyguardInteractor: KeyguardInteractor,
) :
UdfpsLockscreenViewModel(
context,
com.android.internal.R.attr.colorSurface,
com.android.internal.R.attr.materialColorPrimaryFixed,
transitionInteractor,
+ interactor,
+ keyguardInteractor,
)
data class TransitionViewModel(
diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
index 68cdfb6d5865..66067b11a18c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt
@@ -4,10 +4,11 @@ import android.hardware.face.FaceManager
import android.hardware.face.FaceSensorPropertiesInternal
import com.android.keyguard.FaceAuthUiEvent
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.dagger.FaceAuthLog
+import com.google.errorprone.annotations.CompileTimeConstant
import javax.inject.Inject
private const val TAG = "DeviceEntryFaceAuthRepositoryLog"
@@ -240,7 +241,7 @@ constructor(
)
}
- fun hardwareError(errorStatus: ErrorAuthenticationStatus) {
+ fun hardwareError(errorStatus: ErrorFaceAuthenticationStatus) {
logBuffer.log(
TAG,
DEBUG,
@@ -264,4 +265,8 @@ constructor(
fun watchdogScheduled() {
logBuffer.log(TAG, DEBUG, "FaceManager Biometric watchdog scheduled.")
}
+
+ fun faceLockedOut(@CompileTimeConstant reason: String) {
+ logBuffer.log(TAG, DEBUG, "Face auth has been locked out: $reason")
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 8d3c6d5d4947..8f884d24ad21 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -30,6 +30,9 @@ import android.os.IBinder
import android.os.ResultReceiver
import android.os.UserHandle
import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
@@ -54,7 +57,11 @@ class MediaProjectionAppSelectorActivity(
/** This is used to override the dependency in a screenshot test */
@VisibleForTesting
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
-) : ChooserActivity(), MediaProjectionAppSelectorView, MediaProjectionAppSelectorResultHandler {
+) :
+ ChooserActivity(),
+ MediaProjectionAppSelectorView,
+ MediaProjectionAppSelectorResultHandler,
+ LifecycleOwner {
@Inject
constructor(
@@ -62,6 +69,8 @@ class MediaProjectionAppSelectorActivity(
activityLauncher: AsyncActivityLauncher
) : this(componentFactory, activityLauncher, listControllerFactory = null)
+ private val lifecycleRegistry = LifecycleRegistry(this)
+ override val lifecycle = lifecycleRegistry
private lateinit var configurationController: ConfigurationController
private lateinit var controller: MediaProjectionAppSelectorController
private lateinit var recentsViewController: MediaProjectionRecentsViewController
@@ -75,7 +84,9 @@ class MediaProjectionAppSelectorActivity(
override fun getLayoutResource() = R.layout.media_projection_app_selector
public override fun onCreate(bundle: Bundle?) {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
component = componentFactory.create(activity = this, view = this, resultHandler = this)
+ component.lifecycleObservers.forEach { lifecycle.addObserver(it) }
// Create a separate configuration controller for this activity as the configuration
// might be different from the global one
@@ -96,6 +107,26 @@ class MediaProjectionAppSelectorActivity(
controller.init()
}
+ override fun onStart() {
+ super.onStart()
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ }
+
+ override fun onPause() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ super.onPause()
+ }
+
+ override fun onStop() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+ super.onStop()
+ }
+
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
configurationController.onConfigurationChanged(newConfig)
@@ -152,6 +183,8 @@ class MediaProjectionAppSelectorActivity(
}
override fun onDestroy() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ component.lifecycleObservers.forEach { lifecycle.removeObserver(it) }
// onDestroy is also called when an app is selected, in that case we only want to send
// RECORD_CONTENT_TASK but not RECORD_CANCEL
if (!taskSelected) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index f6a2f3704283..72352e397806 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -330,6 +330,8 @@ public class MediaProjectionPermissionActivity extends Activity
// Don't send cancel if the user has moved on to the next activity.
if (!mUserSelectingTask) {
finish(RECORD_CANCEL, /* projection= */ null);
+ } else {
+ super.finish();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
index 0b33904829d5..258284e8af51 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt
@@ -31,15 +31,12 @@ import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "RecommendationViewHolder"
/** ViewHolder for a Smartspace media recommendation. */
-class RecommendationViewHolder private constructor(itemView: View, updatedView: Boolean) {
+class RecommendationViewHolder private constructor(itemView: View) {
val recommendations = itemView as TransitionLayout
// Recommendation screen
- lateinit var cardIcon: ImageView
- lateinit var mediaAppIcons: List<CachingIconView>
- lateinit var mediaProgressBars: List<SeekBar>
- lateinit var cardTitle: TextView
+ val cardTitle: TextView = itemView.requireViewById(R.id.media_rec_title)
val mediaCoverContainers =
listOf<ViewGroup>(
@@ -47,53 +44,25 @@ class RecommendationViewHolder private constructor(itemView: View, updatedView:
itemView.requireViewById(R.id.media_cover2_container),
itemView.requireViewById(R.id.media_cover3_container)
)
+ val mediaAppIcons: List<CachingIconView> =
+ mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
val mediaTitles: List<TextView> =
- if (updatedView) {
- mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
- } else {
- listOf(
- itemView.requireViewById(R.id.media_title1),
- itemView.requireViewById(R.id.media_title2),
- itemView.requireViewById(R.id.media_title3)
- )
- }
+ mediaCoverContainers.map { it.requireViewById(R.id.media_title) }
val mediaSubtitles: List<TextView> =
- if (updatedView) {
- mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
- } else {
- listOf(
- itemView.requireViewById(R.id.media_subtitle1),
- itemView.requireViewById(R.id.media_subtitle2),
- itemView.requireViewById(R.id.media_subtitle3)
- )
+ mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) }
+ val mediaProgressBars: List<SeekBar> =
+ mediaCoverContainers.map {
+ it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
+ // Media playback is in the direction of tape, not time, so it stays LTR
+ layoutDirection = View.LAYOUT_DIRECTION_LTR
+ }
}
val mediaCoverItems: List<ImageView> =
- if (updatedView) {
- mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
- } else {
- listOf(
- itemView.requireViewById(R.id.media_cover1),
- itemView.requireViewById(R.id.media_cover2),
- itemView.requireViewById(R.id.media_cover3)
- )
- }
+ mediaCoverContainers.map { it.requireViewById(R.id.media_cover) }
val gutsViewHolder = GutsViewHolder(itemView)
init {
- if (updatedView) {
- mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) }
- cardTitle = itemView.requireViewById(R.id.media_rec_title)
- mediaProgressBars =
- mediaCoverContainers.map {
- it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply {
- // Media playback is in the direction of tape, not time, so it stays LTR
- layoutDirection = View.LAYOUT_DIRECTION_LTR
- }
- }
- } else {
- cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon)
- }
(recommendations.background as IlluminationDrawable).let { background ->
mediaCoverContainers.forEach { background.registerLightSource(it) }
background.registerLightSource(gutsViewHolder.cancel)
@@ -114,63 +83,31 @@ class RecommendationViewHolder private constructor(itemView: View, updatedView:
* @param parent Parent of inflated view.
*/
@JvmStatic
- fun create(
- inflater: LayoutInflater,
- parent: ViewGroup,
- updatedView: Boolean,
- ): RecommendationViewHolder {
+ fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder {
val itemView =
- if (updatedView) {
- inflater.inflate(
- R.layout.media_recommendations,
- parent,
- false /* attachToRoot */
- )
- } else {
- inflater.inflate(
- R.layout.media_smartspace_recommendations,
- parent,
- false /* attachToRoot */
- )
- }
+ inflater.inflate(R.layout.media_recommendations, parent, false /* attachToRoot */)
// Because this media view (a TransitionLayout) is used to measure and layout the views
// in various states before being attached to its parent, we can't depend on the default
// LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
- return RecommendationViewHolder(itemView, updatedView)
+ return RecommendationViewHolder(itemView)
}
// Res Ids for the control components on the recommendation view.
val controlsIds =
setOf(
- R.id.recommendation_card_icon,
R.id.media_rec_title,
- R.id.media_cover1,
- R.id.media_cover2,
- R.id.media_cover3,
R.id.media_cover,
R.id.media_cover1_container,
R.id.media_cover2_container,
R.id.media_cover3_container,
- R.id.media_title1,
- R.id.media_title2,
- R.id.media_title3,
R.id.media_title,
- R.id.media_subtitle1,
- R.id.media_subtitle2,
- R.id.media_subtitle3,
R.id.media_subtitle,
)
val mediaTitlesAndSubtitlesIds =
setOf(
- R.id.media_title1,
- R.id.media_title2,
- R.id.media_title3,
R.id.media_title,
- R.id.media_subtitle1,
- R.id.media_subtitle2,
- R.id.media_subtitle3,
R.id.media_subtitle,
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
index 3fc3ad682bc7..0a5f857af4ad 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
@@ -144,7 +144,7 @@ constructor(
val oldKey: String?,
val controller: MediaController?,
val localMediaManager: LocalMediaManager,
- val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
+ val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager,
) :
LocalMediaManager.DeviceCallback,
MediaController.Callback(),
@@ -180,7 +180,7 @@ constructor(
if (!started) {
localMediaManager.registerCallback(this)
localMediaManager.startScan()
- muteAwaitConnectionManager?.startListening()
+ muteAwaitConnectionManager.startListening()
playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
playbackVolumeControlId = controller?.playbackInfo?.volumeControlId
controller?.registerCallback(this)
@@ -198,7 +198,7 @@ constructor(
controller?.unregisterCallback(this)
localMediaManager.stopScan()
localMediaManager.unregisterCallback(this)
- muteAwaitConnectionManager?.stopListening()
+ muteAwaitConnectionManager.stopListening()
configurationController.removeCallback(configListener)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index 70b5e75e6048..398dcf260dff 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -744,11 +744,7 @@ constructor(
val newRecs = mediaControlPanelFactory.get()
newRecs.attachRecommendation(
- RecommendationViewHolder.create(
- LayoutInflater.from(context),
- mediaContent,
- mediaFlags.isRecommendationCardUpdateEnabled()
- )
+ RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)
)
newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
val lp =
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index a978b92cb234..a12bc2c99d63 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -784,14 +784,7 @@ public class MediaControlPanel {
contentDescription =
mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
} else if (data != null) {
- if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
- contentDescription = mContext.getString(
- R.string.controls_media_smartspace_rec_header);
- } else {
- contentDescription = mContext.getString(
- R.string.controls_media_smartspace_rec_description,
- data.getAppName(mContext));
- }
+ contentDescription = mContext.getString(R.string.controls_media_smartspace_rec_header);
} else {
contentDescription = null;
}
@@ -1377,10 +1370,6 @@ public class MediaControlPanel {
PackageManager packageManager = mContext.getPackageManager();
// Set up media source app's logo.
Drawable icon = packageManager.getApplicationIcon(applicationInfo);
- if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
- ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
- headerLogoImageView.setImageDrawable(icon);
- }
fetchAndUpdateRecommendationColors(icon);
// Set up media rec card's tap action if applicable.
@@ -1401,16 +1390,7 @@ public class MediaControlPanel {
// Set up media item cover.
ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
- if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
- bindRecommendationArtwork(
- recommendation,
- data.getPackageName(),
- itemIndex
- );
- } else {
- mediaCoverImageView.post(
- () -> mediaCoverImageView.setImageIcon(recommendation.getIcon()));
- }
+ bindRecommendationArtwork(recommendation, data.getPackageName(), itemIndex);
// Set up the media item's click listener if applicable.
ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
@@ -1455,21 +1435,18 @@ public class MediaControlPanel {
subtitleView.setText(subtitle);
// Set up progress bar
- if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
- SeekBar mediaProgressBar =
- mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
- TextView mediaSubtitle =
- mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
- // show progress bar if the recommended album is played.
- Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
- if (progress == null || progress <= 0.0) {
- mediaProgressBar.setVisibility(View.GONE);
- mediaSubtitle.setVisibility(View.VISIBLE);
- } else {
- mediaProgressBar.setProgress((int) (progress * 100));
- mediaProgressBar.setVisibility(View.VISIBLE);
- mediaSubtitle.setVisibility(View.GONE);
- }
+ SeekBar mediaProgressBar =
+ mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
+ TextView mediaSubtitle = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
+ // show progress bar if the recommended album is played.
+ Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
+ if (progress == null || progress <= 0.0) {
+ mediaProgressBar.setVisibility(View.GONE);
+ mediaSubtitle.setVisibility(View.VISIBLE);
+ } else {
+ mediaProgressBar.setProgress((int) (progress * 100));
+ mediaProgressBar.setVisibility(View.VISIBLE);
+ mediaSubtitle.setVisibility(View.GONE);
}
}
mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
@@ -1588,9 +1565,7 @@ public class MediaControlPanel {
int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
- if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
- mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
- }
+ mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
mRecommendationViewHolder.getRecommendations()
.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
@@ -1598,12 +1573,9 @@ public class MediaControlPanel {
(title) -> title.setTextColor(textPrimaryColor));
mRecommendationViewHolder.getMediaSubtitles().forEach(
(subtitle) -> subtitle.setTextColor(textSecondaryColor));
- if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) {
- mRecommendationViewHolder.getMediaProgressBars().forEach(
- (progressBar) -> progressBar.setProgressTintList(
- ColorStateList.valueOf(textPrimaryColor))
- );
- }
+ mRecommendationViewHolder.getMediaProgressBars().forEach(
+ (progressBar) -> progressBar.setProgressTintList(
+ ColorStateList.valueOf(textPrimaryColor)));
mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 4bca778b77c5..1dd969f9bea5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -655,13 +655,8 @@ constructor(
expandedLayout.load(context, R.xml.media_session_expanded)
}
TYPE.RECOMMENDATION -> {
- if (mediaFlags.isRecommendationCardUpdateEnabled()) {
- collapsedLayout.load(context, R.xml.media_recommendations_view_collapsed)
- expandedLayout.load(context, R.xml.media_recommendations_view_expanded)
- } else {
- collapsedLayout.load(context, R.xml.media_recommendation_collapsed)
- expandedLayout.load(context, R.xml.media_recommendation_expanded)
- }
+ collapsedLayout.load(context, R.xml.media_recommendations_collapsed)
+ expandedLayout.load(context, R.xml.media_recommendations_expanded)
}
}
refreshState()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 01f047ccd4f5..f2db088ced83 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -34,25 +34,12 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
return enabled || featureFlags.isEnabled(Flags.MEDIA_SESSION_ACTIONS)
}
- /** Check whether we support displaying information about mute await connections. */
- fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT)
-
- /**
- * Check whether we enable support for nearby media devices. See
- * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information.
- */
- fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES)
-
/**
* If true, keep active media controls for the lifetime of the MediaSession, regardless of
* whether the underlying notification was dismissed
*/
fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS)
- /** Check whether we show the updated recommendation card. */
- fun isRecommendationCardUpdateEnabled() =
- featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)
-
/** Check whether to get progress information for resume players */
fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
index 46efac56ab9d..888cd0bf8b9a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java
@@ -23,10 +23,7 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.media.controls.ui.MediaHost;
import com.android.systemui.media.controls.ui.MediaHostStatesManager;
-import com.android.systemui.media.controls.util.MediaFlags;
import com.android.systemui.media.dream.dagger.MediaComplicationComponent;
-import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
-import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags;
import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogBuffer;
@@ -119,29 +116,4 @@ public interface MediaModule {
}
return Optional.of(helperLazy.get());
}
-
- /** */
- @Provides
- @SysUISingleton
- static Optional<MediaMuteAwaitConnectionCli> providesMediaMuteAwaitConnectionCli(
- MediaFlags mediaFlags,
- Lazy<MediaMuteAwaitConnectionCli> muteAwaitConnectionCliLazy
- ) {
- if (!mediaFlags.areMuteAwaitConnectionsEnabled()) {
- return Optional.empty();
- }
- return Optional.of(muteAwaitConnectionCliLazy.get());
- }
-
- /** */
- @Provides
- @SysUISingleton
- static Optional<NearbyMediaDevicesManager> providesNearbyMediaDevicesManager(
- MediaFlags mediaFlags,
- Lazy<NearbyMediaDevicesManager> nearbyMediaDevicesManagerLazy) {
- if (!mediaFlags.areNearbyMediaDevicesEnabled()) {
- return Optional.empty();
- }
- return Optional.of(nearbyMediaDevicesManagerLazy.get());
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
index a1e9995dd695..18d51030197a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
@@ -31,7 +31,6 @@ import com.android.systemui.media.nearby.NearbyMediaDevicesManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import java.util.Optional
import javax.inject.Inject
/**
@@ -46,7 +45,7 @@ class MediaOutputBroadcastDialogFactory @Inject constructor(
private val notifCollection: CommonNotifCollection,
private val uiEventLogger: UiEventLogger,
private val dialogLaunchAnimator: DialogLaunchAnimator,
- private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
+ private val nearbyMediaDevicesManager: NearbyMediaDevicesManager,
private val audioManager: AudioManager,
private val powerExemptionManager: PowerExemptionManager,
private val keyGuardManager: KeyguardManager,
@@ -62,7 +61,7 @@ class MediaOutputBroadcastDialogFactory @Inject constructor(
val controller = MediaOutputController(context, packageName,
mediaSessionManager, lbm, starter, notifCollection,
- dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
+ dialogLaunchAnimator, nearbyMediaDevicesManager, audioManager,
powerExemptionManager, keyGuardManager, featureFlags, userTracker)
val dialog =
MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index cc75478ef506..b6ca0b025a14 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -99,7 +99,6 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -177,7 +176,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback,
lbm, ActivityStarter starter,
CommonNotifCollection notifCollection,
DialogLaunchAnimator dialogLaunchAnimator,
- Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional,
+ NearbyMediaDevicesManager nearbyMediaDevicesManager,
AudioManager audioManager,
PowerExemptionManager powerExemptionManager,
KeyguardManager keyGuardManager,
@@ -198,7 +197,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback,
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
mDialogLaunchAnimator = dialogLaunchAnimator;
- mNearbyMediaDevicesManager = nearbyMediaDevicesManagerOptional.orElse(null);
+ mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
mColorItemContent = Utils.getColorStateListDefaultColor(mContext,
R.color.media_dialog_item_main_content);
mColorSeekbarProgress = Utils.getColorStateListDefaultColor(mContext,
@@ -927,7 +926,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback,
void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) {
MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
- mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager),
+ mNotifCollection, mDialogLaunchAnimator, mNearbyMediaDevicesManager,
mAudioManager, mPowerExemptionManager, mKeyGuardManager, mFeatureFlags,
mUserTracker);
MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 4c168ecb81af..af659378e8f7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -33,7 +33,6 @@ import com.android.systemui.media.nearby.NearbyMediaDevicesManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import java.util.Optional
import javax.inject.Inject
/**
@@ -48,7 +47,7 @@ class MediaOutputDialogFactory @Inject constructor(
private val notifCollection: CommonNotifCollection,
private val uiEventLogger: UiEventLogger,
private val dialogLaunchAnimator: DialogLaunchAnimator,
- private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
+ private val nearbyMediaDevicesManager: NearbyMediaDevicesManager,
private val audioManager: AudioManager,
private val powerExemptionManager: PowerExemptionManager,
private val keyGuardManager: KeyguardManager,
@@ -68,7 +67,7 @@ class MediaOutputDialogFactory @Inject constructor(
val controller = MediaOutputController(
context, packageName,
mediaSessionManager, lbm, starter, notifCollection,
- dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
+ dialogLaunchAnimator, nearbyMediaDevicesManager, audioManager,
powerExemptionManager, keyGuardManager, featureFlags, userTracker)
val dialog =
MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller,
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
index e26089450c21..97ec654a627b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactory.kt
@@ -21,14 +21,12 @@ import com.android.settingslib.media.DeviceIconUtil
import com.android.settingslib.media.LocalMediaManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.media.controls.util.MediaFlags
import java.util.concurrent.Executor
import javax.inject.Inject
/** Factory class to create [MediaMuteAwaitConnectionManager] instances. */
@SysUISingleton
class MediaMuteAwaitConnectionManagerFactory @Inject constructor(
- private val mediaFlags: MediaFlags,
private val context: Context,
private val logger: MediaMuteAwaitLogger,
@Main private val mainExecutor: Executor
@@ -36,10 +34,7 @@ class MediaMuteAwaitConnectionManagerFactory @Inject constructor(
private val deviceIconUtil = DeviceIconUtil()
/** Creates a [MediaMuteAwaitConnectionManager]. */
- fun create(localMediaManager: LocalMediaManager): MediaMuteAwaitConnectionManager? {
- if (!mediaFlags.areMuteAwaitConnectionsEnabled()) {
- return null
- }
+ fun create(localMediaManager: LocalMediaManager): MediaMuteAwaitConnectionManager {
return MediaMuteAwaitConnectionManager(
mainExecutor, localMediaManager, context, deviceIconUtil, logger
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 60504e43465a..8a565fa86b35 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -30,8 +30,4 @@ class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags)
/** Check whether the flag for the receiver success state is enabled. */
fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
-
- /** True if the media transfer chip can be dismissed via a gesture. */
- fun isMediaTttDismissGestureEnabled(): Boolean =
- featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER_DISMISS_GESTURE)
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index 3088d8b58023..11538fadf24e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -20,6 +20,7 @@ import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.os.UserHandle
+import androidx.lifecycle.DefaultLifecycleObserver
import com.android.launcher3.icons.IconFactory
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.MediaProjectionAppSelectorActivity
@@ -46,6 +47,7 @@ import dagger.Provides
import dagger.Subcomponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
import javax.inject.Qualifier
import javax.inject.Scope
import kotlinx.coroutines.CoroutineScope
@@ -100,6 +102,12 @@ interface MediaProjectionAppSelectorModule {
@MediaProjectionAppSelectorScope
fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader
+ @Binds
+ @IntoSet
+ fun taskPreviewSizeProviderAsLifecycleObserver(
+ impl: TaskPreviewSizeProvider
+ ): DefaultLifecycleObserver
+
companion object {
@Provides
@MediaProjectionAppSelector
@@ -166,4 +174,5 @@ interface MediaProjectionAppSelectorComponent {
@get:PersonalProfile val personalProfileUserHandle: UserHandle
@MediaProjectionAppSelector val configurationController: ConfigurationController
+ val lifecycleObservers: Set<DefaultLifecycleObserver>
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt
index 89f66b7daaf8..864d35af41b4 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt
@@ -21,6 +21,8 @@ import android.content.res.Configuration
import android.graphics.Rect
import android.view.WindowInsets.Type
import android.view.WindowManager
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
import com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen
@@ -35,18 +37,22 @@ class TaskPreviewSizeProvider
constructor(
private val context: Context,
private val windowManager: WindowManager,
- configurationController: ConfigurationController
-) : CallbackController<TaskPreviewSizeListener>, ConfigurationListener {
+ private val configurationController: ConfigurationController,
+) : CallbackController<TaskPreviewSizeListener>, ConfigurationListener, DefaultLifecycleObserver {
/** Returns the size of the task preview on the screen in pixels */
val size: Rect = calculateSize()
private val listeners = arrayListOf<TaskPreviewSizeListener>()
- init {
+ override fun onCreate(owner: LifecycleOwner) {
configurationController.addCallback(this)
}
+ override fun onDestroy(owner: LifecycleOwner) {
+ configurationController.removeCallback(this)
+ }
+
override fun onConfigChanged(newConfig: Configuration) {
val newSize = calculateSize()
if (newSize != size) {
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
new file mode 100644
index 000000000000..c74a71c52260
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.model
+
+import com.android.systemui.dagger.qualifiers.DisplayId
+
+/**
+ * In-bulk updates multiple flag values and commits the update.
+ *
+ * Example:
+ * ```
+ * sysuiState.updateFlags(
+ * displayId,
+ * SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ * SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ * SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ * SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ * SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == SceneKey.Lockscreen),
+ * )
+ * ```
+ *
+ * You can inject [displayId] by injecting it using:
+ * ```
+ * @DisplayId private val displayId: Int`,
+ * ```
+ */
+fun SysUiState.updateFlags(
+ @DisplayId displayId: Int,
+ vararg flagValuePairs: Pair<Int, Boolean>,
+) {
+ flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
+ commitUpdate(displayId)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
deleted file mode 100644
index 86f0c0d15b55..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.remoteproxy
-
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-
-/**
- * Acts as a hub for routing proxied user input into the multi shade system.
- *
- * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
- * In other words: it's not user input that's occurring directly on the shade UI itself. This class
- * is that proxy.
- */
-@Singleton
-class MultiShadeInputProxy @Inject constructor() {
- private val _proxiedTouch =
- MutableSharedFlow<ProxiedInputModel>(
- replay = 1,
- onBufferOverflow = BufferOverflow.DROP_OLDEST,
- )
- val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow()
-
- fun onProxiedInput(proxiedInput: ProxiedInputModel) {
- _proxiedTouch.tryEmit(proxiedInput)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
deleted file mode 100644
index 117203012757..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.repository
-
-import android.content.Context
-import androidx.annotation.FloatRange
-import com.android.systemui.R
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.multishade.shared.model.ShadeModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-
-/** Encapsulates application state for all shades. */
-@SysUISingleton
-class MultiShadeRepository
-@Inject
-constructor(
- @Application private val applicationContext: Context,
- inputProxy: MultiShadeInputProxy,
-) {
- /**
- * Remote input coming from sources outside of system UI (for example, swiping down on the
- * Launcher or from the status bar).
- */
- val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput
-
- /** Width of the left-hand side shade, in pixels. */
- private val leftShadeWidthPx =
- applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width)
-
- /** Width of the right-hand side shade, in pixels. */
- private val rightShadeWidthPx =
- applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width)
-
- /**
- * The amount that the user must swipe up when the shade is fully expanded to automatically
- * collapse once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully expanded once the user stops swiping.
- *
- * This is a fraction between `0` and `1`.
- */
- private val swipeCollapseThreshold =
- checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold))
-
- /**
- * The amount that the user must swipe down when the shade is fully collapsed to automatically
- * expand once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully collapsed once the user stops swiping.
- *
- * This is a fraction between `0` and `1`.
- */
- private val swipeExpandThreshold =
- checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold))
-
- /**
- * Maximum opacity when the scrim that shows up behind the dual shades is fully visible.
- *
- * This is a fraction between `0` and `1`.
- */
- private val dualShadeScrimAlpha =
- checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha))
-
- /** The current configuration of the shade system. */
- val shadeConfig: StateFlow<ShadeConfig> =
- MutableStateFlow(
- if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) {
- ShadeConfig.DualShadeConfig(
- leftShadeWidthPx = leftShadeWidthPx,
- rightShadeWidthPx = rightShadeWidthPx,
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- splitFraction =
- applicationContext.resources.getFloat(
- R.dimen.dual_shade_split_fraction
- ),
- scrimAlpha = dualShadeScrimAlpha,
- )
- } else {
- ShadeConfig.SingleShadeConfig(
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- )
- }
- )
- .asStateFlow()
-
- private val _forceCollapseAll = MutableStateFlow(false)
- /** Whether all shades should be collapsed. */
- val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow()
-
- private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null)
- /** The current shade interaction or `null` if no shade is interacted with currently. */
- val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow()
-
- private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>()
-
- /** The model for the shade with the given ID. */
- fun getShade(
- shadeId: ShadeId,
- ): StateFlow<ShadeModel> {
- return getMutableShade(shadeId).asStateFlow()
- }
-
- /** Sets the expansion amount for the shade with the given ID. */
- fun setExpansion(
- shadeId: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) expansion: Float,
- ) {
- getMutableShade(shadeId).let { mutableState ->
- mutableState.value = mutableState.value.copy(expansion = expansion)
- }
- }
-
- /** Sets whether all shades should be immediately forced to collapse. */
- fun setForceCollapseAll(isForced: Boolean) {
- _forceCollapseAll.value = isForced
- }
-
- /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */
- fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) {
- _shadeInteraction.value = shadeInteraction
- }
-
- private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> {
- return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) }
- }
-
- /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */
- private fun checkInBounds(float: Float): Float {
- check(float in 0f..1f) { "$float isn't between 0 and 1." }
- return float
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
deleted file mode 100644
index ebb8639b8922..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import androidx.annotation.FloatRange
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.shared.math.isZero
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.multishade.shared.model.ShadeModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.yield
-
-/** Encapsulates business logic related to interactions with the multi-shade system. */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MultiShadeInteractor
-@Inject
-constructor(
- @Application private val applicationScope: CoroutineScope,
- private val repository: MultiShadeRepository,
- private val inputProxy: MultiShadeInputProxy,
-) {
- /** The current configuration of the shade system. */
- val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig
-
- /** The expansion of the shade that's most expanded. */
- val maxShadeExpansion: Flow<Float> =
- repository.shadeConfig.flatMapLatest { shadeConfig ->
- combine(allShades(shadeConfig)) { shadeModels ->
- shadeModels.maxOfOrNull { it.expansion } ?: 0f
- }
- }
-
- /** Whether any shade is expanded, even a little bit. */
- val isAnyShadeExpanded: Flow<Boolean> =
- maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged()
-
- /**
- * A _processed_ version of the proxied input flow.
- *
- * All internal dependencies on the proxied input flow *must* use this one for two reasons:
- * 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we
- * actually have.
- * 2. It actually does some preprocessing as the proxied input events stream through, handling
- * common things like recording the current state of the system based on incoming input
- * events.
- */
- private val processedProxiedInput: SharedFlow<ProxiedInputModel> =
- combine(
- repository.shadeConfig,
- repository.proxiedInput.distinctUntilChanged(),
- ::Pair,
- )
- .map { (shadeConfig, proxiedInput) ->
- if (proxiedInput !is ProxiedInputModel.OnTap) {
- // If the user is interacting with any other gesture type (for instance,
- // dragging),
- // we no longer want to force collapse all shades.
- repository.setForceCollapseAll(false)
- }
-
- when (proxiedInput) {
- is ProxiedInputModel.OnDrag -> {
- val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction)
- // This might be the start of a new drag gesture, let's update our
- // application
- // state to record that fact.
- onUserInteractionStarted(
- shadeId = affectedShadeId,
- isProxied = true,
- )
- }
- is ProxiedInputModel.OnTap -> {
- // Tapping outside any shade collapses all shades. This code path is not hit
- // for
- // taps that happen _inside_ a shade as that input event is directly applied
- // through the UI and is, hence, not a proxied input.
- collapseAll()
- }
- else -> Unit
- }
-
- proxiedInput
- }
- .shareIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- replay = 1,
- )
-
- /** Whether the shade with the given ID should be visible. */
- fun isVisible(shadeId: ShadeId): Flow<Boolean> {
- return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) }
- }
-
- /** Whether direct user input is allowed on the shade with the given ID. */
- fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> {
- return combine(
- isForceCollapsed(shadeId),
- repository.shadeInteraction,
- ::Pair,
- )
- .map { (isForceCollapsed, shadeInteraction) ->
- !isForceCollapsed && shadeInteraction?.isProxied != true
- }
- }
-
- /** Whether the shade with the given ID is forced to collapse. */
- fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> {
- return combine(
- repository.forceCollapseAll,
- repository.shadeInteraction.map { it?.shadeId },
- ::Pair,
- )
- .map { (collapseAll, userInteractedShadeIdOrNull) ->
- val counterpartShadeIdOrNull =
- when (shadeId) {
- ShadeId.SINGLE -> null
- ShadeId.LEFT -> ShadeId.RIGHT
- ShadeId.RIGHT -> ShadeId.LEFT
- }
-
- when {
- // If all shades have been told to collapse (by a tap outside, for example),
- // then this shade is collapsed.
- collapseAll -> true
- // A shade that doesn't have a counterpart shade cannot be force-collapsed by
- // interactions on the counterpart shade.
- counterpartShadeIdOrNull == null -> false
- // If the current user interaction is on the counterpart shade, then this shade
- // should be force-collapsed.
- else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull
- }
- }
- }
-
- /**
- * Proxied input affecting the shade with the given ID. This is input coming from sources
- * outside of system UI (for example, swiping down on the Launcher or from the status bar) or
- * outside the UI of any shade (for example, the scrim that's shown behind the shades).
- */
- fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> {
- return combine(
- processedProxiedInput,
- isForceCollapsed(shadeId).distinctUntilChanged(),
- repository.shadeInteraction,
- ::Triple,
- )
- .map { (proxiedInput, isForceCollapsed, shadeInteraction) ->
- when {
- // If the shade is force-collapsed, we ignored proxied input on it.
- isForceCollapsed -> null
- // If the proxied input does not belong to this shade, ignore it.
- shadeInteraction?.shadeId != shadeId -> null
- // If there is ongoing non-proxied user input on any shade, ignore the
- // proxied input.
- !shadeInteraction.isProxied -> null
- // Otherwise, send the proxied input downstream.
- else -> proxiedInput
- }
- }
- .onEach { proxiedInput ->
- // We use yield() to make sure that the following block of code happens _after_
- // downstream collectors had a chance to process the proxied input. Otherwise, we
- // might change our state to clear the current UserInteraction _before_ those
- // downstream collectors get a chance to process the proxied input, which will make
- // them ignore it (since they ignore proxied input when the current user interaction
- // doesn't match their shade).
- yield()
-
- if (
- proxiedInput is ProxiedInputModel.OnDragEnd ||
- proxiedInput is ProxiedInputModel.OnDragCancel
- ) {
- onUserInteractionEnded(shadeId = shadeId, isProxied = true)
- }
- }
- }
-
- /** Sets the expansion amount for the shade with the given ID. */
- fun setExpansion(
- shadeId: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) expansion: Float,
- ) {
- repository.setExpansion(shadeId, expansion)
- }
-
- /** Collapses all shades. */
- fun collapseAll() {
- repository.setForceCollapseAll(true)
- }
-
- /**
- * Notifies that a new non-proxied interaction may have started. Safe to call multiple times for
- * the same interaction as it won't overwrite an existing interaction.
- *
- * Existing interactions can be cleared by calling [onUserInteractionEnded].
- */
- fun onUserInteractionStarted(shadeId: ShadeId) {
- onUserInteractionStarted(
- shadeId = shadeId,
- isProxied = false,
- )
- }
-
- /**
- * Notifies that the current non-proxied interaction has ended.
- *
- * Safe to call multiple times, even if there's no current interaction or even if the current
- * interaction doesn't belong to the given shade or is proxied as the code is a no-op unless
- * there's a match between the parameters and the current interaction.
- */
- fun onUserInteractionEnded(
- shadeId: ShadeId,
- ) {
- onUserInteractionEnded(
- shadeId = shadeId,
- isProxied = false,
- )
- }
-
- fun sendProxiedInput(proxiedInput: ProxiedInputModel) {
- inputProxy.onProxiedInput(proxiedInput)
- }
-
- /**
- * Notifies that a new interaction may have started. Safe to call multiple times for the same
- * interaction as it won't overwrite an existing interaction.
- *
- * Existing interactions can be cleared by calling [onUserInteractionEnded].
- */
- private fun onUserInteractionStarted(
- shadeId: ShadeId,
- isProxied: Boolean,
- ) {
- if (repository.shadeInteraction.value != null) {
- return
- }
-
- repository.setShadeInteraction(
- MultiShadeInteractionModel(
- shadeId = shadeId,
- isProxied = isProxied,
- )
- )
- }
-
- /**
- * Notifies that the current interaction has ended.
- *
- * Safe to call multiple times, even if there's no current interaction or even if the current
- * interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op
- * unless there's a match between the parameters and the current interaction.
- */
- private fun onUserInteractionEnded(
- shadeId: ShadeId,
- isProxied: Boolean,
- ) {
- repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) ->
- if (shadeId == interactionShadeId && isProxied == isInteractionProxied) {
- repository.setShadeInteraction(null)
- }
- }
- }
-
- /**
- * Returns the ID of the shade that's affected by user input at a given coordinate.
- *
- * @param config The shade configuration being used.
- * @param xFraction The horizontal position of the user input as a fraction along the width of
- * its container where `0` is all the way to the left and `1` is all the way to the right.
- */
- private fun affectedShadeId(
- config: ShadeConfig,
- @FloatRange(from = 0.0, to = 1.0) xFraction: Float,
- ): ShadeId {
- return if (config is ShadeConfig.DualShadeConfig) {
- if (xFraction <= config.splitFraction) {
- ShadeId.LEFT
- } else {
- ShadeId.RIGHT
- }
- } else {
- ShadeId.SINGLE
- }
- }
-
- /** Returns the list of flows of all the shades in the given configuration. */
- private fun allShades(
- config: ShadeConfig,
- ): List<Flow<ShadeModel>> {
- return config.shadeIds.map { shadeId -> repository.getShade(shadeId) }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt
deleted file mode 100644
index 1894bc4cfeab..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import android.content.Context
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import com.android.systemui.classifier.Classifier
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.multishade.shared.math.isZero
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.shade.ShadeController
-import javax.inject.Inject
-import kotlin.math.abs
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-
-/**
- * Encapsulates business logic to handle [MotionEvent]-based user input.
- *
- * This class is meant purely for the legacy `View`-based system to be able to pass `MotionEvent`s
- * into the newer multi-shade framework for processing.
- */
-@SysUISingleton
-class MultiShadeMotionEventInteractor
-@Inject
-constructor(
- @Application private val applicationContext: Context,
- @Application private val applicationScope: CoroutineScope,
- private val multiShadeInteractor: MultiShadeInteractor,
- featureFlags: FeatureFlags,
- keyguardTransitionInteractor: KeyguardTransitionInteractor,
- private val falsingManager: FalsingManager,
- private val shadeController: ShadeController,
-) {
- init {
- if (featureFlags.isEnabled(Flags.DUAL_SHADE)) {
- applicationScope.launch {
- multiShadeInteractor.isAnyShadeExpanded.collect {
- if (!it && !shadeController.isKeyguard) {
- shadeController.makeExpandedInvisible()
- } else {
- shadeController.makeExpandedVisible(false)
- }
- }
- }
- }
- }
-
- private val isAnyShadeExpanded: StateFlow<Boolean> =
- multiShadeInteractor.isAnyShadeExpanded.stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = false,
- )
-
- private val isBouncerShowing: StateFlow<Boolean> =
- keyguardTransitionInteractor
- .transitionValue(state = KeyguardState.PRIMARY_BOUNCER)
- .map { !it.isZero() }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.Eagerly,
- initialValue = false,
- )
-
- private var interactionState: InteractionState? = null
-
- /**
- * Returns `true` if the given [MotionEvent] and the rest of events in this gesture should be
- * passed to this interactor's [onTouchEvent] method.
- *
- * Note: the caller should continue to pass [MotionEvent] instances into this method, even if it
- * returns `false` as the gesture may be intercepted mid-stream.
- */
- fun shouldIntercept(event: MotionEvent): Boolean {
- if (isAnyShadeExpanded.value) {
- // If any shade is expanded, we assume that touch handling outside the shades is handled
- // by the scrim that appears behind the shades. No need to intercept anything here.
- return false
- }
-
- if (isBouncerShowing.value) {
- return false
- }
-
- return when (event.actionMasked) {
- MotionEvent.ACTION_DOWN -> {
- // Record where the pointer was placed and which pointer it was.
- interactionState =
- InteractionState(
- initialX = event.x,
- initialY = event.y,
- currentY = event.y,
- pointerId = event.getPointerId(0),
- isDraggingHorizontally = false,
- isDraggingShade = false,
- )
-
- false
- }
- MotionEvent.ACTION_MOVE -> {
- onMove(event)
-
- // We want to intercept the rest of the gesture if we're dragging the shade.
- isDraggingShade()
- }
- MotionEvent.ACTION_UP,
- MotionEvent.ACTION_CANCEL ->
- // Make sure that we intercept the up or cancel if we're dragging the shade, to
- // handle drag end or cancel.
- isDraggingShade()
- else -> false
- }
- }
-
- /**
- * Notifies that a [MotionEvent] in a series of events of a gesture that was intercepted due to
- * the result of [shouldIntercept] has been received.
- *
- * @param event The [MotionEvent] to handle.
- * @param viewWidthPx The width of the view, in pixels.
- * @return `true` if the event was consumed, `false` otherwise.
- */
- fun onTouchEvent(event: MotionEvent, viewWidthPx: Int): Boolean {
- return when (event.actionMasked) {
- MotionEvent.ACTION_MOVE -> {
- interactionState?.let {
- if (it.isDraggingShade) {
- val pointerIndex = event.findPointerIndex(it.pointerId)
- val previousY = it.currentY
- val currentY = event.getY(pointerIndex)
- interactionState = it.copy(currentY = currentY)
-
- val yDragAmountPx = currentY - previousY
-
- if (yDragAmountPx != 0f) {
- multiShadeInteractor.sendProxiedInput(
- ProxiedInputModel.OnDrag(
- xFraction = event.x / viewWidthPx,
- yDragAmountPx = yDragAmountPx,
- )
- )
- }
- true
- } else {
- onMove(event)
- isDraggingShade()
- }
- }
- ?: false
- }
- MotionEvent.ACTION_UP -> {
- if (isDraggingShade()) {
- // We finished dragging the shade. Record that so the multi-shade framework can
- // issue a fling, if the velocity reached in the drag was high enough, for
- // example.
- multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragEnd)
-
- if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
- multiShadeInteractor.collapseAll()
- }
- }
-
- interactionState = null
- true
- }
- MotionEvent.ACTION_POINTER_UP -> {
- val removedPointerId = event.getPointerId(event.actionIndex)
- if (removedPointerId == interactionState?.pointerId && event.pointerCount > 1) {
- // We removed the original pointer but there must be another pointer because the
- // gesture is still ongoing. Let's switch to that pointer.
- interactionState =
- event.firstUnremovedPointerId(removedPointerId)?.let { replacementPointerId
- ->
- interactionState?.copy(
- pointerId = replacementPointerId,
- // We want to update the currentY of our state so that the
- // transition to the next pointer doesn't report a big jump between
- // the Y coordinate of the removed pointer and the Y coordinate of
- // the replacement pointer.
- currentY = event.getY(replacementPointerId),
- )
- }
- }
- true
- }
- MotionEvent.ACTION_CANCEL -> {
- if (isDraggingShade()) {
- // Our drag gesture was canceled by the system. This happens primarily in one of
- // two occasions: (a) the parent view has decided to intercept the gesture
- // itself and/or route it to a different child view or (b) the pointer has
- // traveled beyond the bounds of our view and/or the touch display. Either way,
- // we pass the cancellation event to the multi-shade framework to record it.
- // Doing that allows the multi-shade framework to know that the gesture ended to
- // allow new gestures to be accepted.
- multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragCancel)
-
- if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) {
- multiShadeInteractor.collapseAll()
- }
- }
-
- interactionState = null
- true
- }
- else -> false
- }
- }
-
- /**
- * Handles [MotionEvent.ACTION_MOVE] and sets whether or not we are dragging shade in our
- * current interaction
- *
- * @param event The [MotionEvent] to handle.
- */
- private fun onMove(event: MotionEvent) {
- interactionState?.let {
- val pointerIndex = event.findPointerIndex(it.pointerId)
- val currentX = event.getX(pointerIndex)
- val currentY = event.getY(pointerIndex)
- if (!it.isDraggingHorizontally && !it.isDraggingShade) {
- val xDistanceTravelled = currentX - it.initialX
- val yDistanceTravelled = currentY - it.initialY
- val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop
- interactionState =
- when {
- yDistanceTravelled > touchSlop -> it.copy(isDraggingShade = true)
- abs(xDistanceTravelled) > touchSlop ->
- it.copy(isDraggingHorizontally = true)
- else -> interactionState
- }
- }
- }
- }
-
- private data class InteractionState(
- val initialX: Float,
- val initialY: Float,
- val currentY: Float,
- val pointerId: Int,
- /** Whether the current gesture is dragging horizontally. */
- val isDraggingHorizontally: Boolean,
- /** Whether the current gesture is dragging the shade vertically. */
- val isDraggingShade: Boolean,
- )
-
- private fun isDraggingShade(): Boolean {
- return interactionState?.isDraggingShade ?: false
- }
-
- /**
- * Returns the index of the first pointer that is not [removedPointerId] or `null`, if there is
- * no other pointer.
- */
- private fun MotionEvent.firstUnremovedPointerId(removedPointerId: Int): Int? {
- return (0 until pointerCount)
- .firstOrNull { pointerIndex ->
- val pointerId = getPointerId(pointerIndex)
- pointerId != removedPointerId
- }
- ?.let { pointerIndex -> getPointerId(pointerIndex) }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
deleted file mode 100644
index ee1dd65b867f..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.shared.model
-
-import androidx.annotation.FloatRange
-
-/**
- * Models a part of an ongoing proxied user input gesture.
- *
- * "Proxied" user input is coming through a proxy; typically from an external app or different UI.
- * In other words: it's not user input that's occurring directly on the shade UI itself.
- */
-sealed class ProxiedInputModel {
- /** The user is dragging their pointer. */
- data class OnDrag(
- /**
- * The relative position of the pointer as a fraction of its container width where `0` is
- * all the way to the left and `1` is all the way to the right.
- */
- @FloatRange(from = 0.0, to = 1.0) val xFraction: Float,
- /** The amount that the pointer was dragged, in pixels. */
- val yDragAmountPx: Float,
- ) : ProxiedInputModel()
-
- /** The user finished dragging by lifting up their pointer. */
- object OnDragEnd : ProxiedInputModel()
-
- /**
- * The drag gesture has been canceled. Usually because the pointer exited the draggable area.
- */
- object OnDragCancel : ProxiedInputModel()
-
- /** The user has tapped (clicked). */
- object OnTap : ProxiedInputModel()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
deleted file mode 100644
index a4cd35c8a11a..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.shared.model
-
-import androidx.annotation.FloatRange
-
-/** Enumerates the various possible configurations of the shade system. */
-sealed class ShadeConfig(
-
- /** IDs of the shade(s) in this configuration. */
- open val shadeIds: List<ShadeId>,
-
- /**
- * The amount that the user must swipe up when the shade is fully expanded to automatically
- * collapse once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully expanded once the user stops swiping.
- */
- @FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float,
-
- /**
- * The amount that the user must swipe down when the shade is fully collapsed to automatically
- * expand once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully collapsed once the user stops swiping.
- */
- @FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float,
-) {
-
- /** There is a single shade. */
- data class SingleShadeConfig(
- @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
- @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
- ) :
- ShadeConfig(
- shadeIds = listOf(ShadeId.SINGLE),
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- )
-
- /** There are two shades arranged side-by-side. */
- data class DualShadeConfig(
- /** Width of the left-hand side shade. */
- val leftShadeWidthPx: Int,
- /** Width of the right-hand side shade. */
- val rightShadeWidthPx: Int,
- @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float,
- @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float,
- /**
- * The position of the "split" between interaction areas for each of the shades, as a
- * fraction of the width of the container.
- *
- * Interactions that occur on the start-side (left-hand side in left-to-right languages like
- * English) affect the start-side shade. Interactions that occur on the end-side (right-hand
- * side in left-to-right languages like English) affect the end-side shade.
- */
- @FloatRange(from = 0.0, to = 1.0) val splitFraction: Float,
- /** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */
- @FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float,
- ) :
- ShadeConfig(
- shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT),
- swipeCollapseThreshold = swipeCollapseThreshold,
- swipeExpandThreshold = swipeExpandThreshold,
- )
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
deleted file mode 100644
index aecec39c5c07..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.view
-
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.FrameLayout
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.compose.ComposeFacade
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.launch
-
-/**
- * View that hosts the multi-shade system and acts as glue between legacy code and the
- * implementation.
- */
-class MultiShadeView(
- context: Context,
- attrs: AttributeSet?,
-) :
- FrameLayout(
- context,
- attrs,
- ) {
-
- fun init(
- interactor: MultiShadeInteractor,
- clock: SystemClock,
- ) {
- repeatWhenAttached {
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- addView(
- ComposeFacade.createMultiShadeView(
- context = context,
- viewModel =
- MultiShadeViewModel(
- viewModelScope = this,
- interactor = interactor,
- ),
- clock = clock,
- )
- )
- }
-
- // Here when destroyed.
- removeAllViews()
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
deleted file mode 100644
index ed92c5469d23..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Models UI state for UI that supports multi (or single) shade. */
-@OptIn(ExperimentalCoroutinesApi::class)
-class MultiShadeViewModel(
- viewModelScope: CoroutineScope,
- private val interactor: MultiShadeInteractor,
-) {
- /** Models UI state for the single shade. */
- val singleShade =
- ShadeViewModel(
- viewModelScope,
- ShadeId.SINGLE,
- interactor,
- )
-
- /** Models UI state for the shade on the left-hand side. */
- val leftShade =
- ShadeViewModel(
- viewModelScope,
- ShadeId.LEFT,
- interactor,
- )
-
- /** Models UI state for the shade on the right-hand side. */
- val rightShade =
- ShadeViewModel(
- viewModelScope,
- ShadeId.RIGHT,
- interactor,
- )
-
- /** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */
- val scrimAlpha: StateFlow<Float> =
- combine(
- interactor.maxShadeExpansion,
- interactor.shadeConfig
- .map { it as? ShadeConfig.DualShadeConfig }
- .map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f },
- ::Pair,
- )
- .map { (anyShadeExpansion, scrimAlpha) ->
- (anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f)
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = 0f,
- )
-
- /** Whether the scrim should accept touch events. */
- val isScrimEnabled: StateFlow<Boolean> =
- interactor.shadeConfig
- .flatMapLatest { shadeConfig ->
- when (shadeConfig) {
- // In the dual shade configuration, the scrim is enabled when the expansion is
- // greater than zero on any one of the shades.
- is ShadeConfig.DualShadeConfig -> interactor.isAnyShadeExpanded
- // No scrim in the single shade configuration.
- is ShadeConfig.SingleShadeConfig -> flowOf(false)
- }
- }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
-
- /** Notifies that the scrim has been touched. */
- fun onScrimTouched(proxiedInput: ProxiedInputModel) {
- if (!isScrimEnabled.value) {
- return
- }
-
- interactor.sendProxiedInput(proxiedInput)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
deleted file mode 100644
index e828dbdc6c62..000000000000
--- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import androidx.annotation.FloatRange
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Models UI state for a single shade. */
-class ShadeViewModel(
- viewModelScope: CoroutineScope,
- private val shadeId: ShadeId,
- private val interactor: MultiShadeInteractor,
-) {
- /** Whether the shade is visible. */
- val isVisible: StateFlow<Boolean> =
- interactor
- .isVisible(shadeId)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
-
- /** Whether swiping on the shade UI is currently enabled. */
- val isSwipingEnabled: StateFlow<Boolean> =
- interactor
- .isNonProxiedInputAllowed(shadeId)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = false,
- )
-
- /** Whether the shade must be collapsed immediately. */
- val isForceCollapsed: Flow<Boolean> =
- interactor.isForceCollapsed(shadeId).distinctUntilChanged()
-
- /** The width of the shade. */
- val width: StateFlow<Size> =
- interactor.shadeConfig
- .map { shadeWidth(it) }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = shadeWidth(interactor.shadeConfig.value),
- )
-
- /**
- * The amount that the user must swipe up when the shade is fully expanded to automatically
- * collapse once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully expanded once the user stops swiping.
- */
- val swipeCollapseThreshold: StateFlow<Float> =
- interactor.shadeConfig
- .map { it.swipeCollapseThreshold }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = interactor.shadeConfig.value.swipeCollapseThreshold,
- )
-
- /**
- * The amount that the user must swipe down when the shade is fully collapsed to automatically
- * expand once the user lets go of the shade. If the user swipes less than this amount, the
- * shade will automatically revert back to fully collapsed once the user stops swiping.
- */
- val swipeExpandThreshold: StateFlow<Float> =
- interactor.shadeConfig
- .map { it.swipeExpandThreshold }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = interactor.shadeConfig.value.swipeExpandThreshold,
- )
-
- /**
- * Proxied input affecting the shade. This is input coming from sources outside of system UI
- * (for example, swiping down on the Launcher or from the status bar) or outside the UI of any
- * shade (for example, the scrim that's shown behind the shades).
- */
- val proxiedInput: Flow<ProxiedInputModel?> =
- interactor
- .proxiedInput(shadeId)
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null,
- )
-
- /** Notifies that the expansion amount for the shade has changed. */
- fun onExpansionChanged(
- expansion: Float,
- ) {
- interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f))
- }
-
- /** Notifies that a drag gesture has started. */
- fun onDragStarted() {
- interactor.onUserInteractionStarted(shadeId)
- }
-
- /** Notifies that a drag gesture has ended. */
- fun onDragEnded() {
- interactor.onUserInteractionEnded(shadeId = shadeId)
- }
-
- private fun shadeWidth(shadeConfig: ShadeConfig): Size {
- return when (shadeId) {
- ShadeId.LEFT ->
- Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0)
- ShadeId.RIGHT ->
- Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0)
- ShadeId.SINGLE -> Size.Fraction(1f)
- }
- }
-
- sealed class Size {
- data class Fraction(
- @FloatRange(from = 0.0, to = 1.0) val fraction: Float,
- ) : Size()
- data class Pixels(
- val pixels: Int,
- ) : Size()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 682335e0b419..e134f7c10b9b 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -133,6 +133,7 @@ import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.rotation.RotationButton;
@@ -199,6 +200,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
private final SysUiState mSysUiFlagsContainer;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
private final ShadeController mShadeController;
+ private final ShadeViewController mShadeViewController;
private final NotificationRemoteInputManager mNotificationRemoteInputManager;
private final OverviewProxyService mOverviewProxyService;
private final NavigationModeController mNavigationModeController;
@@ -523,6 +525,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
@Inject
NavigationBar(
NavigationBarView navigationBarView,
+ ShadeController shadeController,
NavigationBarFrame navigationBarFrame,
@Nullable Bundle savedState,
@DisplayId Context context,
@@ -541,7 +544,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
Optional<Pip> pipOptional,
Optional<Recents> recentsOptional,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
- ShadeController shadeController,
+ ShadeViewController shadeViewController,
NotificationRemoteInputManager notificationRemoteInputManager,
NotificationShadeDepthController notificationShadeDepthController,
@Main Handler mainHandler,
@@ -577,6 +580,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
mSysUiFlagsContainer = sysUiFlagsContainer;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
mShadeController = shadeController;
+ mShadeViewController = shadeViewController;
mNotificationRemoteInputManager = notificationRemoteInputManager;
mOverviewProxyService = overviewProxyService;
mNavigationModeController = navigationModeController;
@@ -739,8 +743,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
final Display display = mView.getDisplay();
mView.setComponents(mRecentsOptional);
if (mCentralSurfacesOptionalLazy.get().isPresent()) {
- mView.setComponents(
- mCentralSurfacesOptionalLazy.get().get().getShadeViewController());
+ mView.setComponents(mShadeViewController);
}
mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer);
mView.setOnVerticalChangedListener(this::onVerticalChanged);
@@ -1341,9 +1344,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements
}
private void onVerticalChanged(boolean isVertical) {
- Optional<CentralSurfaces> cs = mCentralSurfacesOptionalLazy.get();
- if (cs.isPresent() && cs.get().getShadeViewController() != null) {
- cs.get().getShadeViewController().setQsScrimEnabled(!isVertical);
+ // This check can probably be safely removed. It only remained to reduce regression
+ // risk for a broad change that removed the CentralSurfaces reference in the if block
+ if (mCentralSurfacesOptionalLazy.get().isPresent()) {
+ mShadeViewController.setQsScrimEnabled(!isVertical);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 77e2847cbe76..c4749e093854 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -26,6 +26,7 @@ import android.os.VibrationEffect
import android.util.Log
import android.util.MathUtils
import android.view.Gravity
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
@@ -36,6 +37,8 @@ import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -76,27 +79,24 @@ private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f
private const val POP_ON_INACTIVE_VELOCITY = -1.5f
internal val VIBRATE_ACTIVATED_EFFECT =
- VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
internal val VIBRATE_DEACTIVATED_EFFECT =
- VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
+ VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
private const val DEBUG = false
-class BackPanelController internal constructor(
- context: Context,
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(
- BackPanel(
- context,
- latencyTracker
- )
-), NavigationEdgeBackPlugin {
+class BackPanelController
+internal constructor(
+ context: Context,
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker,
+ private val featureFlags: FeatureFlags
+) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
/**
* Injectable instance to create a new BackPanelController.
@@ -104,34 +104,37 @@ class BackPanelController internal constructor(
* Necessary because EdgeBackGestureHandler sometimes needs to create new instances of
* BackPanelController, and we need to match EdgeBackGestureHandler's context.
*/
- class Factory @Inject constructor(
- private val windowManager: WindowManager,
- private val viewConfiguration: ViewConfiguration,
- @Main private val mainHandler: Handler,
- private val vibratorHelper: VibratorHelper,
- private val configurationController: ConfigurationController,
- private val latencyTracker: LatencyTracker
+ class Factory
+ @Inject
+ constructor(
+ private val windowManager: WindowManager,
+ private val viewConfiguration: ViewConfiguration,
+ @Main private val mainHandler: Handler,
+ private val vibratorHelper: VibratorHelper,
+ private val configurationController: ConfigurationController,
+ private val latencyTracker: LatencyTracker,
+ private val featureFlags: FeatureFlags
) {
- /** Construct a [BackPanelController]. */
+ /** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
- val backPanelController = BackPanelController(
+ val backPanelController =
+ BackPanelController(
context,
windowManager,
viewConfiguration,
mainHandler,
vibratorHelper,
configurationController,
- latencyTracker
- )
+ latencyTracker,
+ featureFlags
+ )
backPanelController.init()
return backPanelController
}
}
- @VisibleForTesting
- internal var params: EdgePanelParams = EdgePanelParams(resources)
- @VisibleForTesting
- internal var currentState: GestureState = GestureState.GONE
+ @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources)
+ @VisibleForTesting internal var currentState: GestureState = GestureState.GONE
private var previousState: GestureState = GestureState.GONE
// Screen attributes
@@ -167,7 +170,6 @@ class BackPanelController internal constructor(
private val elapsedTimeSinceEntry
get() = SystemClock.uptimeMillis() - gestureEntryTime
-
private var pastThresholdWhileEntryOrInactiveTime = 0L
private var entryToActiveDelay = 0F
private val entryToActiveDelayCalculation = {
@@ -206,24 +208,25 @@ class BackPanelController internal constructor(
COMMITTED,
/* back action currently cancelling, arrow soon to be GONE */
- CANCELLED;
+ CANCELLED
}
/**
* Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The
* runnable is not called if the animation is cancelled
*/
- inner class DelayedOnAnimationEndListener internal constructor(
- private val handler: Handler,
- private val runnableDelay: Long,
- val runnable: Runnable,
+ inner class DelayedOnAnimationEndListener
+ internal constructor(
+ private val handler: Handler,
+ private val runnableDelay: Long,
+ val runnable: Runnable,
) : DynamicAnimation.OnAnimationEndListener {
override fun onAnimationEnd(
- animation: DynamicAnimation<*>,
- canceled: Boolean,
- value: Float,
- velocity: Float
+ animation: DynamicAnimation<*>,
+ canceled: Boolean,
+ value: Float,
+ velocity: Float
) {
animation.removeEndListener(this)
@@ -239,45 +242,43 @@ class BackPanelController internal constructor(
internal fun run() = runnable.run()
}
- private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
- updateArrowState(GestureState.COMMITTED)
- }
-
+ private val onEndSetCommittedStateListener =
+ DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) }
private val onEndSetGoneStateListener =
- DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
- cancelFailsafe()
- updateArrowState(GestureState.GONE)
- }
+ DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
+ cancelFailsafe()
+ updateArrowState(GestureState.GONE)
+ }
- private val onAlphaEndSetGoneStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
- updateRestingArrowDimens()
- if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
- scheduleFailsafe()
+ private val onAlphaEndSetGoneStateListener =
+ DelayedOnAnimationEndListener(mainHandler, 0L) {
+ updateRestingArrowDimens()
+ if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+ scheduleFailsafe()
+ }
}
- }
// Minimum of the screen's width or the predefined threshold
private var fullyStretchedThreshold = 0f
- /**
- * Used for initialization and configuration changes
- */
+ /** Used for initialization and configuration changes */
private fun updateConfiguration() {
params.update(resources)
mView.updateArrowPaint(params.arrowThickness)
minFlingDistance = viewConfiguration.scaledTouchSlop * 3
}
- private val configurationListener = object : ConfigurationController.ConfigurationListener {
- override fun onConfigChanged(newConfig: Configuration?) {
- updateConfiguration()
- }
+ private val configurationListener =
+ object : ConfigurationController.ConfigurationListener {
+ override fun onConfigChanged(newConfig: Configuration?) {
+ updateConfiguration()
+ }
- override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
- updateArrowDirection(isLayoutRtl)
+ override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
+ updateArrowDirection(isLayoutRtl)
+ }
}
- }
override fun onViewAttached() {
updateConfiguration()
@@ -320,8 +321,9 @@ class BackPanelController internal constructor(
MotionEvent.ACTION_UP -> {
when (currentState) {
GestureState.ENTRY -> {
- if (isFlungAwayFromEdge(endX = event.x) ||
- previousXTranslation > params.staticTriggerThreshold
+ if (
+ isFlungAwayFromEdge(endX = event.x) ||
+ previousXTranslation > params.staticTriggerThreshold
) {
updateArrowState(GestureState.FLUNG)
} else {
@@ -342,14 +344,16 @@ class BackPanelController internal constructor(
}
}
GestureState.ACTIVE -> {
- if (previousState == GestureState.ENTRY &&
- elapsedTimeSinceEntry
- < MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
+ if (
+ previousState == GestureState.ENTRY &&
+ elapsedTimeSinceEntry <
+ MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
) {
updateArrowState(GestureState.FLUNG)
- } else if (previousState == GestureState.INACTIVE &&
- elapsedTimeSinceInactive
- < MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
+ } else if (
+ previousState == GestureState.INACTIVE &&
+ elapsedTimeSinceInactive <
+ MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
) {
// A delay is added to allow the background to transition back to ACTIVE
// since it was briefly in INACTIVE. Without this delay, setting it
@@ -390,10 +394,10 @@ class BackPanelController internal constructor(
}
/**
- * Returns false until the current gesture exceeds the touch slop threshold,
- * and returns true thereafter (we reset on the subsequent back gesture).
- * The moment it switches from false -> true is important,
- * because that's when we switch state, from GONE -> ENTRY.
+ * Returns false until the current gesture exceeds the touch slop threshold, and returns true
+ * thereafter (we reset on the subsequent back gesture). The moment it switches from false ->
+ * true is important, because that's when we switch state, from GONE -> ENTRY.
+ *
* @return whether the current gesture has moved past a minimum threshold.
*/
private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
@@ -416,7 +420,8 @@ class BackPanelController internal constructor(
val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold
when (currentState) {
GestureState.ENTRY -> {
- if (isPastThresholdToActive(
+ if (
+ isPastThresholdToActive(
isPastThreshold = isPastStaticThreshold,
dynamicDelay = entryToActiveDelayCalculation
)
@@ -428,8 +433,10 @@ class BackPanelController internal constructor(
val isPastDynamicReactivationThreshold =
totalTouchDeltaInactive >= params.reactivationTriggerThreshold
- if (isPastThresholdToActive(
- isPastThreshold = isPastStaticThreshold &&
+ if (
+ isPastThresholdToActive(
+ isPastThreshold =
+ isPastStaticThreshold &&
isPastDynamicReactivationThreshold &&
isWithinYActivationThreshold,
delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
@@ -489,19 +496,19 @@ class BackPanelController internal constructor(
// Add a slop to to prevent small jitters when arrow is at edge in
// emitting small values that cause the arrow to poke out slightly
val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat()
- totalTouchDeltaInactive = totalTouchDeltaInactive
- .plus(xDelta)
- .coerceAtLeast(minimumDelta)
+ totalTouchDeltaInactive =
+ totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta)
}
updateArrowStateOnMove(yTranslation, xTranslation)
- val gestureProgress = when (currentState) {
- GestureState.ACTIVE -> fullScreenProgress(xTranslation)
- GestureState.ENTRY -> staticThresholdProgress(xTranslation)
- GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
- else -> null
- }
+ val gestureProgress =
+ when (currentState) {
+ GestureState.ACTIVE -> fullScreenProgress(xTranslation)
+ GestureState.ENTRY -> staticThresholdProgress(xTranslation)
+ GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
+ else -> null
+ }
gestureProgress?.let {
when (currentState) {
@@ -517,27 +524,30 @@ class BackPanelController internal constructor(
}
private fun setArrowStrokeAlpha(gestureProgress: Float?) {
- val strokeAlphaProgress = when (currentState) {
- GestureState.ENTRY -> gestureProgress
- GestureState.INACTIVE -> gestureProgress
- GestureState.ACTIVE,
- GestureState.FLUNG,
- GestureState.COMMITTED -> 1f
- GestureState.CANCELLED,
- GestureState.GONE -> 0f
- }
+ val strokeAlphaProgress =
+ when (currentState) {
+ GestureState.ENTRY -> gestureProgress
+ GestureState.INACTIVE -> gestureProgress
+ GestureState.ACTIVE,
+ GestureState.FLUNG,
+ GestureState.COMMITTED -> 1f
+ GestureState.CANCELLED,
+ GestureState.GONE -> 0f
+ }
- val indicator = when (currentState) {
- GestureState.ENTRY -> params.entryIndicator
- GestureState.INACTIVE -> params.preThresholdIndicator
- GestureState.ACTIVE -> params.activeIndicator
- else -> params.preThresholdIndicator
- }
+ val indicator =
+ when (currentState) {
+ GestureState.ENTRY -> params.entryIndicator
+ GestureState.INACTIVE -> params.preThresholdIndicator
+ GestureState.ACTIVE -> params.activeIndicator
+ else -> params.preThresholdIndicator
+ }
strokeAlphaProgress?.let { progress ->
- indicator.arrowDimens.alphaSpring?.get(progress)?.takeIf { it.isNewState }?.let {
- mView.popArrowAlpha(0f, it.value)
- }
+ indicator.arrowDimens.alphaSpring
+ ?.get(progress)
+ ?.takeIf { it.isNewState }
+ ?.let { mView.popArrowAlpha(0f, it.value) }
}
}
@@ -546,15 +556,16 @@ class BackPanelController internal constructor(
val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
val rubberbandAmount = 15f
val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
- val yPosition = params.verticalTranslationInterpolator.getInterpolation(yProgress) *
+ val yPosition =
+ params.verticalTranslationInterpolator.getInterpolation(yProgress) *
maxYOffset *
sign(yOffset)
mView.animateVertically(yPosition)
}
/**
- * Tracks the relative position of the drag from the time after the arrow is activated until
- * the arrow is fully stretched (between 0.0 - 1.0f)
+ * Tracks the relative position of the drag from the time after the arrow is activated until the
+ * arrow is fully stretched (between 0.0 - 1.0f)
*/
private fun fullScreenProgress(xTranslation: Float): Float {
val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold
@@ -575,35 +586,32 @@ class BackPanelController internal constructor(
private fun stretchActiveBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = params.horizontalTranslationInterpolator
- .getInterpolation(progress),
- arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
- backgroundWidthStretchAmount = params.activeWidthInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- backgroundHeightStretchAmount = 1f,
- arrowAlphaStretchAmount = 1f,
- edgeCornerStretchAmount = 1f,
- farCornerStretchAmount = 1f,
- fullyStretchedDimens = params.fullyStretchedIndicator
+ horizontalTranslationStretchAmount =
+ params.horizontalTranslationInterpolator.getInterpolation(progress),
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount =
+ params.activeWidthInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ backgroundHeightStretchAmount = 1f,
+ arrowAlphaStretchAmount = 1f,
+ edgeCornerStretchAmount = 1f,
+ farCornerStretchAmount = 1f,
+ fullyStretchedDimens = params.fullyStretchedIndicator
)
}
private fun stretchEntryBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = params.arrowAngleInterpolator
- .getInterpolation(progress),
- backgroundWidthStretchAmount = params.entryWidthInterpolator
- .getInterpolation(progress),
- backgroundHeightStretchAmount = params.heightInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- arrowAlphaStretchAmount = params.entryIndicator.arrowDimens
- .alphaInterpolator?.get(progress)?.value ?: 0f,
- edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
- farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
- fullyStretchedDimens = params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount =
+ params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -612,31 +620,32 @@ class BackPanelController internal constructor(
val interpolator = run {
val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
if (isPastSlop) {
- if (totalTouchDeltaInactive > 0) {
- params.entryWidthInterpolator
+ if (totalTouchDeltaInactive > 0) {
+ params.entryWidthInterpolator
+ } else {
+ params.entryWidthTowardsEdgeInterpolator
+ }
} else {
- params.entryWidthTowardsEdgeInterpolator
+ previousPreThresholdWidthInterpolator
}
- } else {
- previousPreThresholdWidthInterpolator
- }.also { previousPreThresholdWidthInterpolator = it }
+ .also { previousPreThresholdWidthInterpolator = it }
}
return interpolator.getInterpolation(progress).coerceAtLeast(0f)
}
private fun stretchInactiveBackIndicator(progress: Float) {
mView.setStretch(
- horizontalTranslationStretchAmount = 0f,
- arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
- backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
- backgroundHeightStretchAmount = params.heightInterpolator
- .getInterpolation(progress),
- backgroundAlphaStretchAmount = 1f,
- arrowAlphaStretchAmount = params.preThresholdIndicator.arrowDimens
- .alphaInterpolator?.get(progress)?.value ?: 0f,
- edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
- farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
- fullyStretchedDimens = params.preThresholdIndicator
+ horizontalTranslationStretchAmount = 0f,
+ arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+ backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+ backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
+ backgroundAlphaStretchAmount = 1f,
+ arrowAlphaStretchAmount =
+ params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value
+ ?: 0f,
+ edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+ farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+ fullyStretchedDimens = params.preThresholdIndicator
)
}
@@ -647,11 +656,12 @@ class BackPanelController internal constructor(
override fun setIsLeftPanel(isLeftPanel: Boolean) {
mView.isLeftPanel = isLeftPanel
- layoutParams.gravity = if (isLeftPanel) {
- Gravity.LEFT or Gravity.TOP
- } else {
- Gravity.RIGHT or Gravity.TOP
- }
+ layoutParams.gravity =
+ if (isLeftPanel) {
+ Gravity.LEFT or Gravity.TOP
+ } else {
+ Gravity.RIGHT or Gravity.TOP
+ }
}
override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
@@ -667,12 +677,14 @@ class BackPanelController internal constructor(
private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX
- val flingVelocity = velocityTracker?.run {
- computeCurrentVelocity(PX_PER_SEC)
- xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
- } ?: 0f
+ val flingVelocity =
+ velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_SEC)
+ xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+ }
+ ?: 0f
val isPastFlingVelocityThreshold =
- flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
+ flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
}
@@ -699,8 +711,8 @@ class BackPanelController internal constructor(
}
private fun playWithBackgroundWidthAnimation(
- onEnd: DelayedOnAnimationEndListener,
- delay: Long = 0L
+ onEnd: DelayedOnAnimationEndListener,
+ delay: Long = 0L
) {
if (delay == 0L) {
updateRestingArrowDimens()
@@ -724,104 +736,103 @@ class BackPanelController internal constructor(
fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold)
}
- /**
- * Updates resting arrow and background size not accounting for stretch
- */
+ /** Updates resting arrow and background size not accounting for stretch */
private fun updateRestingArrowDimens() {
when (currentState) {
GestureState.GONE,
GestureState.ENTRY -> {
mView.setSpring(
- arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
- scale = params.entryIndicator.scaleSpring,
- verticalTranslation = params.entryIndicator.verticalTranslationSpring,
- horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
- backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
- backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+ scale = params.entryIndicator.scaleSpring,
+ verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+ horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+ backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.entryIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.INACTIVE -> {
mView.setSpring(
- arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
- horizontalTranslation = params.preThresholdIndicator
- .horizontalTranslationSpring,
- scale = params.preThresholdIndicator.scaleSpring,
- backgroundWidth = params.preThresholdIndicator.backgroundDimens
- .widthSpring,
- backgroundHeight = params.preThresholdIndicator.backgroundDimens
- .heightSpring,
- backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+ horizontalTranslation =
+ params.preThresholdIndicator.horizontalTranslationSpring,
+ scale = params.preThresholdIndicator.scaleSpring,
+ backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.ACTIVE -> {
mView.setSpring(
- arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
- scale = params.activeIndicator.scaleSpring,
- horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
- backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+ scale = params.activeIndicator.scaleSpring,
+ horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+ backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.activeIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.FLUNG -> {
mView.setSpring(
- arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
- backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+ backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.flungIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.COMMITTED -> {
mView.setSpring(
- arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
- arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
- scale = params.committedIndicator.scaleSpring,
- backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
- backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
- backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
- backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
- .edgeCornerRadiusSpring,
- backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
- .farCornerRadiusSpring,
+ arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+ arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+ scale = params.committedIndicator.scaleSpring,
+ backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
+ backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+ backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+ backgroundEdgeCornerRadius =
+ params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring,
+ backgroundFarCornerRadius =
+ params.committedIndicator.backgroundDimens.farCornerRadiusSpring,
)
}
GestureState.CANCELLED -> {
mView.setSpring(
- backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring)
+ backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring
+ )
}
else -> {}
}
mView.setRestingDimens(
- animate = !(currentState == GestureState.FLUNG ||
- currentState == GestureState.COMMITTED),
- restingParams = EdgePanelParams.BackIndicatorDimens(
- scale = when (currentState) {
+ animate =
+ !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED),
+ restingParams =
+ EdgePanelParams.BackIndicatorDimens(
+ scale =
+ when (currentState) {
GestureState.ACTIVE,
- GestureState.FLUNG,
- -> params.activeIndicator.scale
+ GestureState.FLUNG, -> params.activeIndicator.scale
GestureState.COMMITTED -> params.committedIndicator.scale
else -> params.preThresholdIndicator.scale
},
- scalePivotX = when (currentState) {
+ scalePivotX =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE,
@@ -830,7 +841,8 @@ class BackPanelController internal constructor(
GestureState.FLUNG,
GestureState.COMMITTED -> params.committedIndicator.scalePivotX
},
- horizontalTranslation = when (currentState) {
+ horizontalTranslation =
+ when (currentState) {
GestureState.GONE -> {
params.activeIndicator.backgroundDimens.width?.times(-1)
}
@@ -843,7 +855,8 @@ class BackPanelController internal constructor(
}
else -> null
},
- arrowDimens = when (currentState) {
+ arrowDimens =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.arrowDimens
@@ -852,7 +865,8 @@ class BackPanelController internal constructor(
GestureState.COMMITTED -> params.committedIndicator.arrowDimens
GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
},
- backgroundDimens = when (currentState) {
+ backgroundDimens =
+ when (currentState) {
GestureState.GONE,
GestureState.ENTRY,
GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
@@ -894,7 +908,7 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
backCallback.setTriggerBack(true)
}
- GestureState.GONE -> { }
+ GestureState.GONE -> {}
}
when (currentState) {
@@ -913,18 +927,25 @@ class BackPanelController internal constructor(
GestureState.ACTIVE -> {
previousXTranslationOnActiveOffset = previousXTranslation
updateRestingArrowDimens()
- vibratorHelper.cancel()
- mainHandler.postDelayed(10L) {
- vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
- }
- val popVelocity = if (previousState == GestureState.INACTIVE) {
- POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
+ if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ vibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
+ )
} else {
- POP_ON_ENTRY_TO_ACTIVE_VELOCITY
+ vibratorHelper.cancel()
+ mainHandler.postDelayed(10L) {
+ vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+ }
}
+ val popVelocity =
+ if (previousState == GestureState.INACTIVE) {
+ POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
+ } else {
+ POP_ON_ENTRY_TO_ACTIVE_VELOCITY
+ }
mView.popOffEdge(popVelocity)
}
-
GestureState.INACTIVE -> {
gestureInactiveTime = SystemClock.uptimeMillis()
@@ -937,7 +958,14 @@ class BackPanelController internal constructor(
mView.popOffEdge(POP_ON_INACTIVE_VELOCITY)
- vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ vibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
+ )
+ } else {
+ vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+ }
updateRestingArrowDimens()
}
GestureState.FLUNG -> {
@@ -945,8 +973,10 @@ class BackPanelController internal constructor(
mView.popScale(POP_ON_FLING_VELOCITY)
}
updateRestingArrowDimens()
- mainHandler.postDelayed(onEndSetCommittedStateListener.runnable,
- MIN_DURATION_FLING_ANIMATION)
+ mainHandler.postDelayed(
+ onEndSetCommittedStateListener.runnable,
+ MIN_DURATION_FLING_ANIMATION
+ )
}
GestureState.COMMITTED -> {
// In most cases, animating between states is handled via `updateRestingArrowDimens`
@@ -956,36 +986,43 @@ class BackPanelController internal constructor(
// manually play these kinds of animations in parallel.
if (previousState == GestureState.FLUNG) {
updateRestingArrowDimens()
- mainHandler.postDelayed(onEndSetGoneStateListener.runnable,
- MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION)
+ mainHandler.postDelayed(
+ onEndSetGoneStateListener.runnable,
+ MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION
+ )
} else {
mView.popScale(POP_ON_COMMITTED_VELOCITY)
- mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable,
- MIN_DURATION_COMMITTED_ANIMATION)
+ mainHandler.postDelayed(
+ onAlphaEndSetGoneStateListener.runnable,
+ MIN_DURATION_COMMITTED_ANIMATION
+ )
}
}
GestureState.CANCELLED -> {
val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
- val springForceOnCancelled = params.cancelledIndicator
- .arrowDimens.alphaSpring?.get(0f)?.value
+ val springForceOnCancelled =
+ params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value
mView.popArrowAlpha(0f, springForceOnCancelled)
- mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+ if (!featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION))
+ mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
}
}
}
private fun convertVelocityToAnimationFactor(
- valueOnFastVelocity: Float,
- valueOnSlowVelocity: Float,
- fastVelocityBound: Float = 1f,
- slowVelocityBound: Float = 0.5f,
+ valueOnFastVelocity: Float,
+ valueOnSlowVelocity: Float,
+ fastVelocityBound: Float = 1f,
+ slowVelocityBound: Float = 0.5f,
): Float {
- val factor = velocityTracker?.run {
- computeCurrentVelocity(PX_PER_MS)
- MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
- } ?: valueOnFastVelocity
+ val factor =
+ velocityTracker?.run {
+ computeCurrentVelocity(PX_PER_MS)
+ MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
+ }
+ ?: valueOnFastVelocity
return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
}
@@ -1014,77 +1051,76 @@ class BackPanelController internal constructor(
}
init {
- if (DEBUG) mView.drawDebugInfo = { canvas ->
- val debugStrings = listOf(
- "$currentState",
- "startX=$startX",
- "startY=$startY",
- "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
- "xTranslation=${"%.1f".format(previousXTranslation)}",
- "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
- "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
- )
- val debugPaint = Paint().apply {
- color = Color.WHITE
- }
- val debugInfoBottom = debugStrings.size * 32f + 4f
- canvas.drawRect(
+ if (DEBUG)
+ mView.drawDebugInfo = { canvas ->
+ val preProgress = staticThresholdProgress(previousXTranslation) * 100
+ val postProgress = fullScreenProgress(previousXTranslation) * 100
+ val debugStrings =
+ listOf(
+ "$currentState",
+ "startX=$startX",
+ "startY=$startY",
+ "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
+ "xTranslation=${"%.1f".format(previousXTranslation)}",
+ "pre=${"%.0f".format(preProgress)}%",
+ "post=${"%.0f".format(postProgress)}%"
+ )
+ val debugPaint = Paint().apply { color = Color.WHITE }
+ val debugInfoBottom = debugStrings.size * 32f + 4f
+ canvas.drawRect(
4f,
4f,
canvas.width.toFloat(),
debugStrings.size * 32f + 4f,
debugPaint
- )
- debugPaint.apply {
- color = Color.BLACK
- textSize = 32f
- }
- var offset = 32f
- for (debugText in debugStrings) {
- canvas.drawText(debugText, 10f, offset, debugPaint)
- offset += 32f
- }
- debugPaint.apply {
- color = Color.RED
- style = Paint.Style.STROKE
- strokeWidth = 4f
- }
- val canvasWidth = canvas.width.toFloat()
- val canvasHeight = canvas.height.toFloat()
- canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
-
- fun drawVerticalLine(x: Float, color: Int) {
- debugPaint.color = color
- val x = if (mView.isLeftPanel) x else canvasWidth - x
- canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
- }
+ )
+ debugPaint.apply {
+ color = Color.BLACK
+ textSize = 32f
+ }
+ var offset = 32f
+ for (debugText in debugStrings) {
+ canvas.drawText(debugText, 10f, offset, debugPaint)
+ offset += 32f
+ }
+ debugPaint.apply {
+ color = Color.RED
+ style = Paint.Style.STROKE
+ strokeWidth = 4f
+ }
+ val canvasWidth = canvas.width.toFloat()
+ val canvasHeight = canvas.height.toFloat()
+ canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
+
+ fun drawVerticalLine(x: Float, color: Int) {
+ debugPaint.color = color
+ val x = if (mView.isLeftPanel) x else canvasWidth - x
+ canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
+ }
- drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
- drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
- drawVerticalLine(x = startX, color = Color.GREEN)
- drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
- }
+ drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
+ drawVerticalLine(x = startX, color = Color.GREEN)
+ drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
+ }
}
}
/**
- * In addition to a typical step function which returns one or two
- * values based on a threshold, `Step` also gracefully handles quick
- * changes in input near the threshold value that would typically
- * result in the output rapidly changing.
+ * In addition to a typical step function which returns one or two values based on a threshold,
+ * `Step` also gracefully handles quick changes in input near the threshold value that would
+ * typically result in the output rapidly changing.
*
- * In the context of Back arrow, the arrow's stroke opacity should
- * always appear transparent or opaque. Using a typical Step function,
- * this would resulting in a flickering appearance as the output would
- * change rapidly. `Step` addresses this by moving the threshold after
- * it is crossed so it cannot be easily crossed again with small changes
- * in touch events.
+ * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or
+ * opaque. Using a typical Step function, this would resulting in a flickering appearance as the
+ * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so
+ * it cannot be easily crossed again with small changes in touch events.
*/
class Step<T>(
- private val threshold: Float,
- private val factor: Float = 1.1f,
- private val postThreshold: T,
- private val preThreshold: T
+ private val threshold: Float,
+ private val factor: Float = 1.1f,
+ private val postThreshold: T,
+ private val preThreshold: T
) {
data class Value<T>(val value: T, val isNewState: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 48790c23e688..2adc211ef23f 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -41,6 +41,8 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
+import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser
import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
@@ -121,23 +123,26 @@ constructor(
/**
* Returns the [UserHandle] of an android user that should handle the notes taking [entryPoint].
- *
- * On company owned personally enabled (COPE) devices, if the given [entryPoint] is in the
- * [FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES] list, the default notes app in the work
- * profile user will always be launched.
- *
- * On non managed devices or devices with other management modes, the current [UserHandle] is
- * returned.
+ * 1. tail button entry point: In COPE or work profile devices, the user can select whether the
+ * work or main profile notes app should be launched in the Settings app. In non-management
+ * or device owner devices, the user can only select main profile notes app.
+ * 2. lock screen quick affordance: since there is no user setting, the main profile notes app
+ * is used as default for work profile devices while the work profile notes app is used for
+ * COPE devices.
+ * 3. Other entry point: the current user from [UserTracker.userHandle].
*/
fun getUserForHandlingNotesTaking(entryPoint: NoteTaskEntryPoint): UserHandle =
- if (
- entryPoint in FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES &&
- devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile
- ) {
- userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle
- ?: userTracker.userHandle
- } else {
- secureSettings.preferredUser
+ when {
+ entryPoint == TAIL_BUTTON -> secureSettings.preferredUser
+ devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile &&
+ entryPoint == QUICK_AFFORDANCE -> {
+ userTracker.userProfiles
+ .firstOrNull { userManager.isManagedProfile(it.id) }
+ ?.userHandle
+ ?: userTracker.userHandle
+ }
+ // On work profile devices, SysUI always run in the main user.
+ else -> userTracker.userHandle
}
/**
@@ -267,15 +272,7 @@ constructor(
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
- // If the required user matches the tracking user, the injected context is already a context
- // of the required user. Avoid calling #createContextAsUser because creating a context for
- // a user takes time.
- val userContext =
- if (user == userTracker.userHandle) {
- context
- } else {
- context.createContextAsUser(user, /* flags= */ 0)
- }
+ val userContext = context.createContextAsUser(user, /* flags= */ 0)
userContext.packageManager.setComponentEnabledSetting(
componentName,
@@ -283,7 +280,7 @@ constructor(
PackageManager.DONT_KILL_APP,
)
- debugLog { "setNoteTaskShortcutEnabled - completed: $isEnabled" }
+ debugLog { "setNoteTaskShortcutEnabled for user $user- completed: $enabledState" }
}
/**
@@ -359,10 +356,12 @@ constructor(
private val SecureSettings.preferredUser: UserHandle
get() {
+ val trackingUserId = userTracker.userHandle.identifier
val userId =
- secureSettings.getInt(
- Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
- userTracker.userHandle.identifier,
+ secureSettings.getIntForUser(
+ /* name= */ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
+ /* def= */ trackingUserId,
+ /* userHandle= */ trackingUserId,
)
return UserHandle.of(userId)
}
@@ -381,16 +380,6 @@ constructor(
* @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE
*/
const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package"
-
- /**
- * A list of entry points which should be redirected to the work profile default notes app
- * on company owned personally enabled (COPE) devices.
- *
- * Entry points in this list don't let users / admin to select the work or personal default
- * notes app to be launched.
- */
- val FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES =
- listOf(NoteTaskEntryPoint.TAIL_BUTTON, NoteTaskEntryPoint.QUICK_AFFORDANCE)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index eb1ca66f6ca8..809edc09070a 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -70,6 +70,19 @@ constructor(
}
}
+ /**
+ * Wakes up the device if dreaming with a screensaver.
+ *
+ * @param why a string explaining why we're waking the device for debugging purposes. Should be
+ * in SCREAMING_SNAKE_CASE.
+ * @param wakeReason the PowerManager-based reason why we're waking the device.
+ */
+ fun wakeUpIfDreaming(why: String, @PowerManager.WakeReason wakeReason: Int) {
+ if (statusBarStateController.isDreaming) {
+ repository.wakeUp(why, wakeReason)
+ }
+ }
+
companion object {
private const val FSI_WAKE_WHY = "full_screen_intent"
}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
new file mode 100644
index 000000000000..fdc70a83e8b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.Manifest
+import android.app.ActivityManager
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.view.View
+import androidx.annotation.MainThread
+import androidx.annotation.WorkerThread
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private val defaultDialogProvider =
+ object : PrivacyDialogControllerV2.DialogProvider {
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2 {
+ return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ }
+ }
+
+/**
+ * Controller for [PrivacyDialogV2].
+ *
+ * This controller shows and dismissed the dialog, as well as determining the information to show in
+ * it.
+ */
+@SysUISingleton
+class PrivacyDialogControllerV2(
+ private val permissionManager: PermissionManager,
+ private val packageManager: PackageManager,
+ private val privacyItemController: PrivacyItemController,
+ private val userTracker: UserTracker,
+ private val activityStarter: ActivityStarter,
+ private val backgroundExecutor: Executor,
+ private val uiExecutor: Executor,
+ private val privacyLogger: PrivacyLogger,
+ private val keyguardStateController: KeyguardStateController,
+ private val appOpsController: AppOpsController,
+ private val uiEventLogger: UiEventLogger,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val dialogProvider: DialogProvider
+) {
+
+ @Inject
+ constructor(
+ permissionManager: PermissionManager,
+ packageManager: PackageManager,
+ privacyItemController: PrivacyItemController,
+ userTracker: UserTracker,
+ activityStarter: ActivityStarter,
+ @Background backgroundExecutor: Executor,
+ @Main uiExecutor: Executor,
+ privacyLogger: PrivacyLogger,
+ keyguardStateController: KeyguardStateController,
+ appOpsController: AppOpsController,
+ uiEventLogger: UiEventLogger,
+ dialogLaunchAnimator: DialogLaunchAnimator
+ ) : this(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogLaunchAnimator,
+ defaultDialogProvider
+ )
+
+ private var dialog: Dialog? = null
+
+ private val onDialogDismissed =
+ object : PrivacyDialogV2.OnDialogDismissed {
+ override fun onDialogDismissed() {
+ privacyLogger.logPrivacyDialogDismissed()
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ dialog = null
+ }
+ }
+
+ @WorkerThread
+ private fun closeApp(packageName: String, userId: Int) {
+ uiEventLogger.log(
+ PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
+ userId,
+ packageName
+ )
+ privacyLogger.logCloseAppFromDialog(packageName, userId)
+ ActivityManager.getService().stopAppForUser(packageName, userId)
+ }
+
+ @MainThread
+ private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) {
+ uiEventLogger.log(
+ PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ userId,
+ packageName
+ )
+ privacyLogger.logStartSettingsActivityFromDialog(packageName, userId)
+ startActivity(navigationIntent)
+ }
+
+ @MainThread
+ private fun openPrivacyDashboard() {
+ uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
+ privacyLogger.logStartPrivacyDashboardFromDialog()
+ startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE))
+ }
+
+ @MainThread
+ private fun startActivity(navigationIntent: Intent) {
+ if (!keyguardStateController.isUnlocked) {
+ // If we are locked, hide the dialog so the user can unlock
+ dialog?.hide()
+ }
+ // startActivity calls internally startActivityDismissingKeyguard
+ activityStarter.startActivity(navigationIntent, true) {
+ if (ActivityManager.isStartResultSuccessful(it)) {
+ dismissDialog()
+ } else {
+ dialog?.show()
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun getStartViewPermissionUsageIntent(
+ packageName: String,
+ permGroupName: String,
+ attributionTag: CharSequence?,
+ isAttributionSupported: Boolean
+ ): Intent? {
+ if (attributionTag != null && isAttributionSupported) {
+ val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ intent.setPackage(packageName)
+ intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName)
+ intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString()))
+ intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true)
+ val resolveInfo =
+ packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0))
+ if (
+ resolveInfo?.activityInfo?.permission ==
+ Manifest.permission.START_VIEW_PERMISSION_USAGE
+ ) {
+ intent.component = ComponentName(packageName, resolveInfo.activityInfo.name)
+ return intent
+ }
+ }
+ return null
+ }
+
+ fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent {
+ val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS)
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
+ intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId))
+ return intent
+ }
+
+ @WorkerThread
+ private fun permGroupUsage(): List<PermissionGroupUsage> {
+ return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted)
+ }
+
+ /**
+ * Show the [PrivacyDialogV2]
+ *
+ * This retrieves the permission usage from [PermissionManager] and creates a new
+ * [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show.
+ *
+ * This list will be filtered by [filterAndSelect]. Only types available by
+ * [PrivacyItemController] will be shown.
+ *
+ * @param context A context to use to create the dialog.
+ * @see filterAndSelect
+ */
+ fun showDialog(context: Context, view: View? = null) {
+ dismissDialog()
+ backgroundExecutor.execute {
+ val usage = permGroupUsage()
+ val userInfos = userTracker.userProfiles
+ privacyLogger.logUnfilteredPermGroupUsage(usage)
+ val items =
+ usage.mapNotNull {
+ val userInfo =
+ userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) }
+ if (
+ isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall)
+ ) {
+ // Only try to get the app name if we actually need it
+ val appName =
+ if (it.isPhoneCall) {
+ ""
+ } else {
+ getLabelForPackage(it.packageName, it.uid)
+ }
+ val userId = UserHandle.getUserId(it.uid)
+ val viewUsageIntent =
+ getStartViewPermissionUsageIntent(
+ it.packageName,
+ it.permissionGroupName,
+ it.attributionTag,
+ // attributionLabel is set only when subattribution policies
+ // are supported and satisfied
+ it.attributionLabel != null
+ )
+ PrivacyDialogV2.PrivacyElement(
+ permGroupToPrivacyType(it.permissionGroupName)!!,
+ it.packageName,
+ userId,
+ appName,
+ it.attributionTag,
+ it.attributionLabel,
+ it.proxyLabel,
+ it.lastAccessTimeMillis,
+ it.isActive,
+ it.isPhoneCall,
+ viewUsageIntent != null,
+ it.permissionGroupName,
+ viewUsageIntent
+ ?: getDefaultManageAppPermissionsIntent(it.packageName, userId)
+ )
+ } else {
+ null
+ }
+ }
+ uiExecutor.execute {
+ val elements = filterAndSelect(items)
+ if (elements.isNotEmpty()) {
+ val d =
+ dialogProvider.makeDialog(
+ context,
+ elements,
+ this::manageApp,
+ this::closeApp,
+ this::openPrivacyDashboard
+ )
+ d.setShowForAllUsers(true)
+ d.addOnDismissListener(onDialogDismissed)
+ if (view != null) {
+ dialogLaunchAnimator.showFromView(d, view)
+ } else {
+ d.show()
+ }
+ privacyLogger.logShowDialogV2Contents(elements)
+ dialog = d
+ } else {
+ privacyLogger.logEmptyDialog()
+ }
+ }
+ }
+ }
+
+ /** Dismisses the dialog */
+ fun dismissDialog() {
+ dialog?.dismiss()
+ }
+
+ @WorkerThread
+ private fun getLabelForPackage(packageName: String, uid: Int): CharSequence {
+ return try {
+ packageManager
+ .getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid))
+ .loadLabel(packageManager)
+ } catch (_: PackageManager.NameNotFoundException) {
+ privacyLogger.logLabelNotFound(packageName)
+ packageName
+ }
+ }
+
+ private fun permGroupToPrivacyType(group: String): PrivacyType? {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA
+ Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE
+ Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION
+ else -> null
+ }
+ }
+
+ private fun isAvailable(group: String): Boolean {
+ return when (group) {
+ Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable
+ Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable
+ Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable
+ else -> false
+ }
+ }
+
+ /**
+ * Filters the list of elements to show.
+ *
+ * For each privacy type, it'll return all active elements. If there are no active elements,
+ * it'll return the most recent access
+ */
+ private fun filterAndSelect(
+ list: List<PrivacyDialogV2.PrivacyElement>
+ ): List<PrivacyDialogV2.PrivacyElement> {
+ return list
+ .groupBy { it.type }
+ .toSortedMap()
+ .flatMap { (_, elements) ->
+ val actives = elements.filter { it.isActive }
+ if (actives.isNotEmpty()) {
+ actives.sortedByDescending { it.lastActiveTimestamp }
+ } else {
+ elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) }
+ ?: emptyList()
+ }
+ }
+ }
+
+ /**
+ * Interface to create a [PrivacyDialogV2].
+ *
+ * Can be used to inject a mock creator.
+ */
+ interface DialogProvider {
+ /** Create a [PrivacyDialogV2]. */
+ fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
index 3ecc5a5e5b00..250976cf47a4 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt
@@ -18,13 +18,20 @@ package com.android.systemui.privacy
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.")
PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904),
@UiEvent(doc = "Privacy dialog is dismissed by user.")
- PRIVACY_DIALOG_DISMISSED(905);
+ PRIVACY_DIALOG_DISMISSED(905),
+
+ @UiEvent(doc = "Privacy dialog item is clicked by user to close the app using a sensor.")
+ PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP(1396),
+
+ @UiEvent(doc = "Privacy dialog is clicked by user to see the privacy dashboard.")
+ PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD(1397);
override fun getId() = _id
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
new file mode 100644
index 000000000000..f4aa27d5fcbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt
@@ -0,0 +1,539 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageItemInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.res.Resources.NotFoundException
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Bundle
+import android.text.TextUtils
+import android.util.Log
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.annotation.WorkerThread
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.maybeForceFullscreen
+import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Dialog to show ongoing and recent app ops element.
+ *
+ * @param context A context to create the dialog
+ * @param list list of elements to show in the dialog. The elements will show in the same order they
+ * appear in the list
+ * @param manageApp a callback to start an activity for a given package name, user id, and intent
+ * @param closeApp a callback to close an app for a given package name, user id
+ * @param openPrivacyDashboard a callback to open the privacy dashboard
+ * @see PrivacyDialogControllerV2
+ */
+class PrivacyDialogV2(
+ context: Context,
+ private val list: List<PrivacyElement>,
+ private val manageApp: (String, Int, Intent) -> Unit,
+ private val closeApp: (String, Int) -> Unit,
+ private val openPrivacyDashboard: () -> Unit
+) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) {
+
+ private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
+ private val dismissed = AtomicBoolean(false)
+ // Note: this will call the dialog create method during init
+ private val decorViewLayoutListener = maybeForceFullscreen()?.component2()
+
+ /**
+ * Add a listener that will be called when the dialog is dismissed.
+ *
+ * If the dialog has already been dismissed, the listener will be called immediately, in the
+ * same thread.
+ */
+ fun addOnDismissListener(listener: OnDialogDismissed) {
+ if (dismissed.get()) {
+ listener.onDialogDismissed()
+ } else {
+ dismissListeners.add(WeakReference(listener))
+ }
+ }
+
+ override fun stop() {
+ dismissed.set(true)
+ val iterator = dismissListeners.iterator()
+ while (iterator.hasNext()) {
+ val el = iterator.next()
+ iterator.remove()
+ el.get()?.onDialogDismissed()
+ }
+ // Remove the layout change listener we may have added to the DecorView.
+ if (decorViewLayoutListener != null) {
+ window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window!!.setGravity(Gravity.CENTER)
+ setTitle(R.string.privacy_dialog_title)
+ setContentView(R.layout.privacy_dialog_v2)
+
+ val closeButton = requireViewById<Button>(R.id.privacy_dialog_close_button)
+ closeButton.setOnClickListener { dismiss() }
+
+ val moreButton = requireViewById<Button>(R.id.privacy_dialog_more_button)
+ moreButton.setOnClickListener { openPrivacyDashboard() }
+
+ val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
+ list.forEach { itemsContainer.addView(createView(it, itemsContainer)) }
+ }
+
+ private fun createView(element: PrivacyElement, itemsContainer: ViewGroup): View {
+ val itemCard =
+ LayoutInflater.from(context)
+ .inflate(R.layout.privacy_dialog_item_v2, itemsContainer, false) as ViewGroup
+
+ updateItemHeader(element, itemCard)
+
+ if (element.isPhoneCall) {
+ return itemCard
+ }
+
+ setItemExpansionBehavior(itemCard)
+
+ configureIndicatorActionButtons(element, itemCard)
+
+ return itemCard
+ }
+
+ private fun updateItemHeader(element: PrivacyElement, itemCard: View) {
+ val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
+ val permGroupLabel = context.packageManager.getDefaultPermGroupLabel(element.permGroupName)
+
+ val iconView = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
+ val indicatorIcon = context.getPermGroupIcon(element.permGroupName)
+ updateIconView(iconView, indicatorIcon, element.isActive)
+ iconView.contentDescription = permGroupLabel
+
+ val titleView = itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_title)!!
+ titleView.text = permGroupLabel
+ titleView.contentDescription = permGroupLabel
+
+ val usageText = getUsageText(element)
+ val summaryView =
+ itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
+ summaryView.text = usageText
+ summaryView.contentDescription = usageText
+ }
+
+ private fun configureIndicatorActionButtons(element: PrivacyElement, itemCard: View) {
+ val expandedLayout =
+ itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header_expanded_layout)!!
+
+ val buttons: MutableList<View> = mutableListOf()
+ configureCloseAppButton(element, expandedLayout)?.also { buttons.add(it) }
+ buttons.add(configureManageButton(element, expandedLayout))
+
+ val backgroundColor = getBackgroundColor(element.isActive)
+ when (buttons.size) {
+ 0 -> return
+ 1 -> {
+ val background =
+ getMutableDrawable(R.drawable.privacy_dialog_background_large_top_large_bottom)
+ background.setTint(backgroundColor)
+ buttons[0].background = background
+ }
+ else -> {
+ val firstBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_large_top_small_bottom)
+ val middleBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_small_top_small_bottom)
+ val lastBackground =
+ getMutableDrawable(R.drawable.privacy_dialog_background_small_top_large_bottom)
+ firstBackground.setTint(backgroundColor)
+ middleBackground.setTint(backgroundColor)
+ lastBackground.setTint(backgroundColor)
+ buttons.forEach { it.background = middleBackground }
+ buttons.first().background = firstBackground
+ buttons.last().background = lastBackground
+ }
+ }
+ }
+
+ private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? {
+ if (element.isService || !element.isActive) {
+ return null
+ }
+ val closeAppButton =
+ window.layoutInflater.inflate(
+ R.layout.privacy_dialog_card_button,
+ expandedLayout,
+ false
+ ) as Button
+ expandedLayout.addView(closeAppButton)
+ closeAppButton.id = R.id.privacy_dialog_close_app_button
+ closeAppButton.setText(R.string.privacy_dialog_close_app_button)
+ closeAppButton.setTextColor(getForegroundColor(true))
+ closeAppButton.tag = element
+ closeAppButton.setOnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ closeApp(element.packageName, element.userId)
+ closeAppTransition(element.packageName, element.userId)
+ }
+ }
+ return closeAppButton
+ }
+
+ private fun closeAppTransition(packageName: String, userId: Int) {
+ val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container)
+ var shouldTransition = false
+ for (i in 0 until itemsContainer.getChildCount()) {
+ val itemCard = itemsContainer.getChildAt(i)
+ val button = itemCard.findViewById<Button>(R.id.privacy_dialog_close_app_button)
+ if (button == null || button.tag == null) {
+ continue
+ }
+ val element = button.tag as PrivacyElement
+ if (element.packageName != packageName || element.userId != userId) {
+ continue
+ }
+
+ itemCard.setEnabled(false)
+
+ val expandToggle =
+ itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
+ expandToggle.visibility = View.GONE
+
+ disableIndicatorCardUi(itemCard, element.applicationName)
+
+ val expandedLayout =
+ itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ shouldTransition = true
+ }
+ }
+ if (shouldTransition) {
+ ViewHierarchyAnimator.animateNextUpdate(window!!.decorView)
+ }
+ }
+
+ private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View {
+ val manageButton =
+ window.layoutInflater.inflate(
+ R.layout.privacy_dialog_card_button,
+ expandedLayout,
+ false
+ ) as Button
+ expandedLayout.addView(manageButton)
+ manageButton.id = R.id.privacy_dialog_manage_app_button
+ manageButton.setText(
+ if (element.isService) R.string.privacy_dialog_manage_service
+ else R.string.privacy_dialog_manage_permissions
+ )
+ manageButton.setTextColor(getForegroundColor(element.isActive))
+ manageButton.tag = element
+ manageButton.setOnClickListener { v ->
+ v.tag?.let {
+ val element = it as PrivacyElement
+ manageApp(element.packageName, element.userId, element.navigationIntent)
+ }
+ }
+ return manageButton
+ }
+
+ private fun disableIndicatorCardUi(itemCard: View, applicationName: CharSequence) {
+ val iconView = itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!!
+ val indicatorIcon = getMutableDrawable(R.drawable.privacy_dialog_check_icon)
+ updateIconView(iconView, indicatorIcon, false)
+
+ val closedAppText =
+ context.getString(R.string.privacy_dialog_close_app_message, applicationName)
+ val summaryView = itemCard.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!!
+ summaryView.text = closedAppText
+ summaryView.contentDescription = closedAppText
+ }
+
+ private fun setItemExpansionBehavior(itemCard: ViewGroup) {
+ val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!!
+
+ val expandToggle =
+ itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!!
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ expandToggle.visibility = View.VISIBLE
+
+ ViewCompat.replaceAccessibilityAction(
+ itemCard,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null
+ )
+
+ val expandedLayout =
+ itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!!
+ expandedLayout.setOnClickListener {
+ // Stop clicks from propagating
+ }
+
+ itemCard.setOnClickListener {
+ if (expandedLayout.visibility == View.VISIBLE) {
+ expandedLayout.visibility = View.GONE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_expand_action),
+ null
+ )
+ } else {
+ expandedLayout.visibility = View.VISIBLE
+ expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up)
+ ViewCompat.replaceAccessibilityAction(
+ it!!,
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK,
+ context.getString(R.string.privacy_dialog_collapse_action),
+ null
+ )
+ }
+ ViewHierarchyAnimator.animateNextUpdate(
+ rootView = window!!.decorView,
+ excludedViews = setOf(expandedLayout)
+ )
+ }
+ }
+
+ private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) {
+ indicatorIcon.setTint(getForegroundColor(active))
+ val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle)
+ backgroundIcon.setTint(getBackgroundColor(active))
+ val backgroundSize =
+ context.resources.getDimension(R.dimen.ongoing_appops_dialog_circle_size).toInt()
+ val indicatorSize =
+ context.resources.getDimension(R.dimen.ongoing_appops_dialog_icon_size).toInt()
+ iconView.setImageDrawable(
+ constructLayeredIcon(indicatorIcon, indicatorSize, backgroundIcon, backgroundSize)
+ )
+ }
+
+ @ColorInt
+ private fun getForegroundColor(active: Boolean) =
+ Utils.getColorAttrDefaultColor(
+ context,
+ if (active) com.android.internal.R.attr.materialColorOnPrimaryFixed
+ else com.android.internal.R.attr.materialColorOnSurface
+ )
+
+ @ColorInt
+ private fun getBackgroundColor(active: Boolean) =
+ Utils.getColorAttrDefaultColor(
+ context,
+ if (active) com.android.internal.R.attr.materialColorPrimaryFixed
+ else com.android.internal.R.attr.materialColorSurfaceContainerHigh
+ )
+
+ private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate()
+
+ private fun getUsageText(element: PrivacyElement) =
+ if (element.isPhoneCall) {
+ val phoneCallResId =
+ if (element.isActive) R.string.privacy_dialog_active_call_usage
+ else R.string.privacy_dialog_recent_call_usage
+ context.getString(phoneCallResId)
+ } else if (element.attributionLabel == null && element.proxyLabel == null) {
+ val usageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage
+ else R.string.privacy_dialog_recent_app_usage
+ context.getString(usageResId, element.applicationName)
+ } else if (element.attributionLabel == null || element.proxyLabel == null) {
+ val singleUsageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage_1
+ else R.string.privacy_dialog_recent_app_usage_1
+ context.getString(
+ singleUsageResId,
+ element.applicationName,
+ element.attributionLabel ?: element.proxyLabel
+ )
+ } else {
+ val doubleUsageResId: Int =
+ if (element.isActive) R.string.privacy_dialog_active_app_usage_2
+ else R.string.privacy_dialog_recent_app_usage_2
+ context.getString(
+ doubleUsageResId,
+ element.applicationName,
+ element.attributionLabel,
+ element.proxyLabel
+ )
+ }
+
+ companion object {
+ private const val LOG_TAG = "PrivacyDialogV2"
+ private const val REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE"
+
+ /**
+ * Gets a permission group's icon from the system.
+ *
+ * @param groupName The name of the permission group whose icon we want
+ * @return The permission group's icon, the privacy_dialog_default_permission_icon icon if
+ * the group has no icon, or the group does not exist
+ */
+ @WorkerThread
+ private fun Context.getPermGroupIcon(groupName: String): Drawable {
+ val groupInfo = packageManager.getGroupInfo(groupName)
+ if (groupInfo != null && groupInfo.icon != 0) {
+ val icon = packageManager.loadDrawable(groupInfo.packageName, groupInfo.icon)
+ if (icon != null) {
+ return icon
+ }
+ }
+
+ return getDrawable(R.drawable.privacy_dialog_default_permission_icon)!!.mutate()
+ }
+
+ /**
+ * Gets a permission group's label from the system.
+ *
+ * @param groupName The name of the permission group whose label we want
+ * @return The permission group's label, or the group name, if the group is invalid
+ */
+ @WorkerThread
+ private fun PackageManager.getDefaultPermGroupLabel(groupName: String): CharSequence {
+ val groupInfo = getGroupInfo(groupName) ?: return groupName
+ return groupInfo.loadSafeLabel(
+ this,
+ 0f,
+ TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
+ )
+ }
+
+ /**
+ * Get the [infos][PackageItemInfo] for the given permission group.
+ *
+ * @param groupName the group
+ * @return The info of permission group or null if the group does not have runtime
+ * permissions.
+ */
+ @WorkerThread
+ private fun PackageManager.getGroupInfo(groupName: String): PackageItemInfo? {
+ try {
+ return getPermissionGroupInfo(groupName, 0)
+ } catch (e: NameNotFoundException) {
+ /* ignore */
+ }
+ try {
+ return getPermissionInfo(groupName, 0)
+ } catch (e: NameNotFoundException) {
+ /* ignore */
+ }
+ return null
+ }
+
+ @WorkerThread
+ private fun PackageManager.loadDrawable(pkg: String, @DrawableRes resId: Int): Drawable? {
+ return try {
+ getResourcesForApplication(pkg).getDrawable(resId, null)?.mutate()
+ } catch (e: NotFoundException) {
+ Log.w(LOG_TAG, "Couldn't get resource", e)
+ null
+ } catch (e: NameNotFoundException) {
+ Log.w(LOG_TAG, "Couldn't get resource", e)
+ null
+ }
+ }
+
+ private fun constructLayeredIcon(
+ icon: Drawable,
+ iconSize: Int,
+ background: Drawable,
+ backgroundSize: Int
+ ): Drawable {
+ val layered = LayerDrawable(arrayOf(background, icon))
+ layered.setLayerSize(0, backgroundSize, backgroundSize)
+ layered.setLayerGravity(0, Gravity.CENTER)
+ layered.setLayerSize(1, iconSize, iconSize)
+ layered.setLayerGravity(1, Gravity.CENTER)
+ return layered
+ }
+ }
+
+ /** */
+ data class PrivacyElement(
+ val type: PrivacyType,
+ val packageName: String,
+ val userId: Int,
+ val applicationName: CharSequence,
+ val attributionTag: CharSequence?,
+ val attributionLabel: CharSequence?,
+ val proxyLabel: CharSequence?,
+ val lastActiveTimestamp: Long,
+ val isActive: Boolean,
+ val isPhoneCall: Boolean,
+ val isService: Boolean,
+ val permGroupName: String,
+ val navigationIntent: Intent
+ ) {
+ private val builder = StringBuilder("PrivacyElement(")
+
+ init {
+ builder.append("type=${type.logName}")
+ builder.append(", packageName=$packageName")
+ builder.append(", userId=$userId")
+ builder.append(", appName=$applicationName")
+ if (attributionTag != null) {
+ builder.append(", attributionTag=$attributionTag")
+ }
+ if (attributionLabel != null) {
+ builder.append(", attributionLabel=$attributionLabel")
+ }
+ if (proxyLabel != null) {
+ builder.append(", proxyLabel=$proxyLabel")
+ }
+ builder.append(", lastActive=$lastActiveTimestamp")
+ if (isActive) {
+ builder.append(", active")
+ }
+ if (isPhoneCall) {
+ builder.append(", phoneCall")
+ }
+ if (isService) {
+ builder.append(", service")
+ }
+ builder.append(", permGroupName=$permGroupName")
+ builder.append(", navigationIntent=$navigationIntent)")
+ }
+
+ override fun toString(): String = builder.toString()
+ }
+
+ /** */
+ interface OnDialogDismissed {
+ fun onDialogDismissed()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
index f934346d9775..1a4642f4df74 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt
@@ -18,11 +18,12 @@ package com.android.systemui.privacy.logging
import android.icu.text.SimpleDateFormat
import android.permission.PermissionGroupUsage
-import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogMessage
+import com.android.systemui.log.dagger.PrivacyLog
import com.android.systemui.privacy.PrivacyDialog
+import com.android.systemui.privacy.PrivacyDialogV2
import com.android.systemui.privacy.PrivacyItem
import java.util.Locale
import javax.inject.Inject
@@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) {
+ log(LogLevel.INFO, {
+ str1 = contents.toString()
+ }, {
+ "Privacy dialog shown. Contents: $str1"
+ })
+ }
+
fun logEmptyDialog() {
log(LogLevel.WARNING, {}, {
"Trying to show an empty dialog"
@@ -147,6 +156,23 @@ class PrivacyLogger @Inject constructor(
})
}
+ fun logCloseAppFromDialog(packageName: String, userId: Int) {
+ log(LogLevel.INFO, {
+ str1 = packageName
+ int1 = userId
+ }, {
+ "Close app from dialog for packageName=$str1, userId=$int1"
+ })
+ }
+
+ fun logStartPrivacyDashboardFromDialog() {
+ log(LogLevel.INFO, {}, { "Start privacy dashboard from dialog" })
+ }
+
+ fun logLabelNotFound(packageName: String) {
+ log(LogLevel.WARNING, { str1 = packageName }, { "Label not found for: $str1" })
+ }
+
private fun listToString(list: List<PrivacyItem>): String {
return list.joinToString(separator = ", ", transform = PrivacyItem::log)
}
@@ -158,4 +184,4 @@ class PrivacyLogger @Inject constructor(
) {
buffer.log(TAG, logLevel, initializer, printer)
}
-} \ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
index 995c6a476f0d..0941a2082cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt
@@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyChipEvent
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
@@ -26,7 +29,7 @@ import java.util.concurrent.Executor
import javax.inject.Inject
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.shade.ShadeModule.Companion.SHADE_HEADER
+import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import javax.inject.Named
@@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor(
private val uiEventLogger: UiEventLogger,
@Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip,
private val privacyDialogController: PrivacyDialogController,
+ private val privacyDialogControllerV2: PrivacyDialogControllerV2,
private val privacyLogger: PrivacyLogger,
@Named(SHADE_HEADER) private val iconContainer: StatusIconContainer,
private val permissionManager: PermissionManager,
@@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor(
private val appOpsController: AppOpsController,
private val broadcastDispatcher: BroadcastDispatcher,
private val safetyCenterManager: SafetyCenterManager,
- private val deviceProvisionedController: DeviceProvisionedController
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val featureFlags: FeatureFlags
) {
var chipVisibilityListener: ChipVisibilityListener? = null
@@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor(
// If the privacy chip is visible, it means there were some indicators
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK)
if (safetyCenterEnabled) {
- showSafetyCenter()
+ if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) {
+ privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
+ } else {
+ showSafetyCenter()
+ }
} else {
privacyDialogController.showDialog(privacyChip.context)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 83b373d5e626..856a92e85ad7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -58,6 +58,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
private final BrightnessSliderController mBrightnessSliderController;
private final BrightnessMirrorHandler mBrightnessMirrorHandler;
private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private boolean mListening;
private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
@Override
@@ -159,12 +160,15 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
public void setListening(boolean listening, boolean expanded) {
setListening(listening && expanded);
- // Set the listening as soon as the QS fragment starts listening regardless of the
- //expansion, so it will update the current brightness before the slider is visible.
- if (listening) {
- mBrightnessController.registerCallbacks();
- } else {
- mBrightnessController.unregisterCallbacks();
+ if (listening != mListening) {
+ mListening = listening;
+ // Set the listening as soon as the QS fragment starts listening regardless of the
+ //expansion, so it will update the current brightness before the slider is visible.
+ if (listening) {
+ mBrightnessController.registerCallbacks();
+ } else {
+ mBrightnessController.unregisterCallbacks();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/ChevronImageView.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/ChevronImageView.kt
new file mode 100644
index 000000000000..8fd1d6f9496b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/ChevronImageView.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tileimpl
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+
+class ChevronImageView(context: Context, attrs: AttributeSet?) : ImageView(context, attrs) {
+
+ override fun resolveLayoutDirection(): Boolean {
+ val previousLayoutDirection = layoutDirection
+ return super.resolveLayoutDirection().also { resolved ->
+ if (resolved && layoutDirection != previousLayoutDirection) {
+ onRtlPropertiesChanged(layoutDirection)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 207cc1398279..bf40a2d0ad51 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -78,13 +78,14 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.AssistUtils;
import com.android.internal.app.IVoiceInteractionSessionListener;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenshotRequest;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -95,6 +96,8 @@ import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.buttons.KeyButtonView;
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeViewController;
@@ -121,6 +124,7 @@ import java.util.concurrent.Executor;
import java.util.function.Supplier;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* Class to send information from overview to launcher with a binder.
@@ -140,13 +144,17 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
private final Context mContext;
+ private final FeatureFlags mFeatureFlags;
private final Executor mMainExecutor;
private final ShellInterface mShellInterface;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
+ private final Lazy<ShadeViewController> mShadeViewControllerLazy;
private SysUiState mSysUiState;
private final Handler mHandler;
private final Lazy<NavigationBarController> mNavBarControllerLazy;
private final NotificationShadeWindowController mStatusBarWinController;
+ private final Provider<SceneInteractor> mSceneInteractor;
+
private final Runnable mConnectionRunnable = () ->
internalConnectToCurrentUser("runnable: startConnectionToCurrentUser");
private final ComponentName mRecentsComponentName;
@@ -201,11 +209,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
// TODO move this logic to message queue
mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
if (event.getActionMasked() == ACTION_DOWN) {
- ShadeViewController shadeViewController =
- centralSurfaces.getShadeViewController();
- if (shadeViewController != null) {
- shadeViewController.startExpandLatencyTracking();
- }
+ mShadeViewControllerLazy.get().startExpandLatencyTracking();
}
mHandler.post(() -> {
int action = event.getActionMasked();
@@ -213,17 +217,28 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mInputFocusTransferStarted = true;
mInputFocusTransferStartY = event.getY();
mInputFocusTransferStartMillis = event.getEventTime();
- centralSurfaces.onInputFocusTransfer(
- mInputFocusTransferStarted, false /* cancel */,
- 0 /* velocity */);
+
+ // If scene framework is enabled, set the scene container window to
+ // visible and let the touch "slip" into that window.
+ if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ mSceneInteractor.get().setVisible(
+ SceneContainerNames.SYSTEM_UI_DEFAULT, true);
+ } else {
+ centralSurfaces.onInputFocusTransfer(
+ mInputFocusTransferStarted, false /* cancel */,
+ 0 /* velocity */);
+ }
}
if (action == ACTION_UP || action == ACTION_CANCEL) {
mInputFocusTransferStarted = false;
- float velocity = (event.getY() - mInputFocusTransferStartY)
- / (event.getEventTime() - mInputFocusTransferStartMillis);
- centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
- action == ACTION_CANCEL,
- velocity);
+
+ if (!mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ float velocity = (event.getY() - mInputFocusTransferStartY)
+ / (event.getEventTime() - mInputFocusTransferStartMillis);
+ centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted,
+ action == ACTION_CANCEL,
+ velocity);
+ }
}
event.recycle();
});
@@ -552,8 +567,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
ShellInterface shellInterface,
Lazy<NavigationBarController> navBarControllerLazy,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+ Lazy<ShadeViewController> shadeViewControllerLazy,
NavigationModeController navModeController,
- NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
+ NotificationShadeWindowController statusBarWinController,
+ SysUiState sysUiState,
+ Provider<SceneInteractor> sceneInteractor,
UserTracker userTracker,
ScreenLifecycle screenLifecycle,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -561,6 +579,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
DisplayTracker displayTracker,
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
AssistUtils assistUtils,
+ FeatureFlags featureFlags,
DumpManager dumpManager,
Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder
) {
@@ -570,12 +589,15 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
mContext = context;
+ mFeatureFlags = featureFlags;
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
+ mShadeViewControllerLazy = shadeViewControllerLazy;
mHandler = new Handler();
mNavBarControllerLazy = navBarControllerLazy;
mStatusBarWinController = statusBarWinController;
+ mSceneInteractor = sceneInteractor;
mUserTracker = userTracker;
mConnectionBackoffAttempts = 0;
mRecentsComponentName = ComponentName.unflattenFromString(context.getString(
@@ -677,13 +699,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
mNavBarControllerLazy.get().getDefaultNavigationBar();
final NavigationBarView navBarView =
mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId());
- final ShadeViewController panelController =
- mCentralSurfacesOptionalLazy.get()
- .map(CentralSurfaces::getShadeViewController)
- .orElse(null);
if (SysUiState.DEBUG) {
Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment
- + " navBarView=" + navBarView + " panelController=" + panelController);
+ + " navBarView=" + navBarView
+ + " shadeViewController=" + mShadeViewControllerLazy.get());
}
if (navBarFragment != null) {
@@ -692,9 +711,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
if (navBarView != null) {
navBarView.updateDisabledSystemUiStateFlags(mSysUiState);
}
- if (panelController != null) {
- panelController.updateSystemUiStateFlags();
- }
+ mShadeViewControllerLazy.get().updateSystemUiStateFlags();
if (mStatusBarWinController != null) {
mStatusBarWinController.notifyStateChangedCallbacks();
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 4582370679ab..f03f040c206d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -18,11 +18,14 @@ package com.android.systemui.scene.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.scene.shared.model.SceneTransitionModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
/**
* Generic business logic and app state accessors for the scene framework.
@@ -92,4 +95,14 @@ constructor(
fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> {
return repository.sceneTransitions(containerName)
}
+
+ private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null)
+
+ /** A flow of motion events originating from outside of the scene framework. */
+ val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow()
+
+ /** Handles a remote user input. */
+ fun onRemoteUserInput(input: RemoteUserInput) {
+ _remoteUserInput.value = input
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index b23c4ec9aa8b..92384d68157b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -20,14 +20,22 @@ import com.android.systemui.CoreStartable
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.DisplayId
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
+import com.android.systemui.model.updateFlags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -49,12 +57,15 @@ constructor(
private val authenticationInteractor: AuthenticationInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val featureFlags: FeatureFlags,
+ private val sysUiState: SysUiState,
+ @DisplayId private val displayId: Int,
) : CoreStartable {
override fun start() {
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
hydrateVisibility()
automaticallySwitchScenes()
+ hydrateSystemUiState()
}
}
@@ -121,6 +132,27 @@ constructor(
}
}
+ /** Keeps [SysUiState] up-to-date */
+ private fun hydrateSystemUiState() {
+ applicationScope.launch {
+ sceneInteractor
+ .currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ .map { it.key }
+ .distinctUntilChanged()
+ .collect { sceneKey ->
+ sysUiState.updateFlags(
+ displayId,
+ SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
+ (sceneKey == SceneKey.Lockscreen),
+ )
+ }
+ }
+ }
+
private fun switchToScene(targetSceneKey: SceneKey) {
sceneInteractor.setCurrentScene(
containerName = CONTAINER_NAME,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt
new file mode 100644
index 000000000000..680de590a3fc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt
@@ -0,0 +1,35 @@
+package com.android.systemui.scene.shared.model
+
+import android.view.MotionEvent
+
+/** A representation of user input that is used by the scene framework. */
+data class RemoteUserInput(
+ val x: Float,
+ val y: Float,
+ val action: RemoteUserInputAction,
+) {
+ companion object {
+ fun translateMotionEvent(event: MotionEvent): RemoteUserInput {
+ return RemoteUserInput(
+ x = event.x,
+ y = event.y,
+ action =
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> RemoteUserInputAction.DOWN
+ MotionEvent.ACTION_MOVE -> RemoteUserInputAction.MOVE
+ MotionEvent.ACTION_UP -> RemoteUserInputAction.UP
+ MotionEvent.ACTION_CANCEL -> RemoteUserInputAction.CANCEL
+ else -> RemoteUserInputAction.UNKNOWN
+ }
+ )
+ }
+ }
+}
+
+enum class RemoteUserInputAction {
+ DOWN,
+ MOVE,
+ UP,
+ CANCEL,
+ UNKNOWN,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index c456be6e5ab2..b89179289a3d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -2,6 +2,7 @@ package com.android.systemui.scene.ui.view
import android.content.Context
import android.util.AttributeSet
+import android.view.MotionEvent
import android.view.View
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -16,11 +17,15 @@ class SceneWindowRootView(
context,
attrs,
) {
+
+ private lateinit var viewModel: SceneContainerViewModel
+
fun init(
viewModel: SceneContainerViewModel,
containerConfig: SceneContainerConfig,
scenes: Set<Scene>,
) {
+ this.viewModel = viewModel
SceneWindowRootViewBinder.bind(
view = this@SceneWindowRootView,
viewModel = viewModel,
@@ -32,6 +37,14 @@ class SceneWindowRootView(
)
}
+ override fun onTouchEvent(event: MotionEvent?): Boolean {
+ return event?.let {
+ viewModel.onRemoteUserInput(event)
+ true
+ }
+ ?: false
+ }
+
override fun setVisibility(visibility: Int) {
// Do nothing. We don't want external callers to invoke this. Instead, we drive our own
// visibility from our view-binder.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 5aa5feeedcf1..f9324a95c1e5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -16,19 +16,25 @@
package com.android.systemui.scene.ui.view
+import android.view.Gravity
+import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
+import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import java.time.Instant
import kotlinx.coroutines.launch
object SceneWindowRootViewBinder {
@@ -77,6 +83,9 @@ object SceneWindowRootViewBinder {
)
)
+ val legacyView = view.requireViewById<View>(R.id.legacy_window_root)
+ view.addView(createVisibilityToggleView(legacyView))
+
launch {
viewModel.isVisible.collect { isVisible ->
onVisibilityChangedInternal(isVisible)
@@ -89,4 +98,28 @@ object SceneWindowRootViewBinder {
}
}
}
+
+ private var clickCount = 0
+ private var lastClick = Instant.now()
+
+ /**
+ * A temporary UI to toggle on/off the visibility of the given [otherView]. It is toggled by
+ * tapping 5 times in quick succession on the device camera (top center).
+ */
+ // TODO(b/291321285): Remove this when the Flexiglass UI is mature enough to turn off legacy
+ // SysUI altogether.
+ private fun createVisibilityToggleView(otherView: View): View {
+ val toggleView = View(otherView.context)
+ toggleView.layoutParams = FrameLayout.LayoutParams(200, 200, Gravity.CENTER_HORIZONTAL)
+ toggleView.setOnClickListener {
+ val now = Instant.now()
+ clickCount = if (now.minusSeconds(2) > lastClick) 1 else clickCount + 1
+ if (clickCount == 5) {
+ otherView.isVisible = !otherView.isVisible
+ clickCount = 0
+ }
+ lastClick = now
+ }
+ return toggleView
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 8c1ad9b4571b..005f48d9f250 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -16,7 +16,9 @@
package com.android.systemui.scene.ui.viewmodel
+import android.view.MotionEvent
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import kotlinx.coroutines.flow.StateFlow
@@ -26,6 +28,9 @@ class SceneContainerViewModel(
private val interactor: SceneInteractor,
val containerName: String,
) {
+ /** A flow of motion events originating from outside of the scene framework. */
+ val remoteUserInput: StateFlow<RemoteUserInput?> = interactor.remoteUserInput
+
/**
* Keys of all scenes in the container.
*
@@ -49,4 +54,9 @@ class SceneContainerViewModel(
fun setSceneTransitionProgress(progress: Float) {
interactor.setSceneTransitionProgress(containerName, progress)
}
+
+ /** Handles a [MotionEvent] representing remote user input. */
+ fun onRemoteUserInput(event: MotionEvent) {
+ interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 9594ba374fc3..6af9b739da52 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -22,7 +22,6 @@ import static com.android.settingslib.display.BrightnessUtils.convertLinearToGam
import android.animation.ValueAnimator;
import android.annotation.NonNull;
-import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
@@ -31,10 +30,10 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerExecutor;
+import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -43,6 +42,8 @@ import android.service.vr.IVrStateCallbacks;
import android.util.Log;
import android.util.MathUtils;
+import androidx.annotation.Nullable;
+
import com.android.internal.display.BrightnessSynchronizer;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -52,10 +53,13 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
+import com.android.systemui.util.settings.SecureSettings;
import java.util.concurrent.Executor;
-import javax.inject.Inject;
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController {
private static final String TAG = "CentralSurfaces.BrightnessController";
@@ -75,8 +79,11 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
private final DisplayManager mDisplayManager;
private final UserTracker mUserTracker;
private final DisplayTracker mDisplayTracker;
+ @Nullable
private final IVrManager mVrManager;
+ private final SecureSettings mSecureSettings;
+
private final Executor mMainExecutor;
private final Handler mBackgroundHandler;
private final BrightnessObserver mBrightnessObserver;
@@ -106,6 +113,8 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
/** ContentObserver to watch brightness */
private class BrightnessObserver extends ContentObserver {
+ private boolean mObserving = false;
+
BrightnessObserver(Handler handler) {
super(handler);
}
@@ -124,19 +133,17 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
}
public void startObserving() {
- final ContentResolver cr = mContext.getContentResolver();
- cr.unregisterContentObserver(this);
- cr.registerContentObserver(
- BRIGHTNESS_MODE_URI,
- false, this, UserHandle.USER_ALL);
- mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener,
- new HandlerExecutor(mHandler));
+ if (!mObserving) {
+ mObserving = true;
+ mSecureSettings.registerContentObserverForUser(
+ BRIGHTNESS_MODE_URI,
+ false, this, UserHandle.USER_ALL);
+ }
}
public void stopObserving() {
- final ContentResolver cr = mContext.getContentResolver();
- cr.unregisterContentObserver(this);
- mDisplayTracker.removeCallback(mBrightnessListener);
+ mSecureSettings.unregisterContentObserver(this);
+ mObserving = false;
}
}
@@ -159,6 +166,8 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
}
mBrightnessObserver.startObserving();
+ mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener,
+ new HandlerExecutor(mMainHandler));
mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
// Update the slider and mode before attaching the listener so we don't
@@ -166,7 +175,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
mUpdateModeRunnable.run();
mUpdateSliderRunnable.run();
- mHandler.sendEmptyMessage(MSG_ATTACH_LISTENER);
+ mMainHandler.sendEmptyMessage(MSG_ATTACH_LISTENER);
}
};
@@ -187,9 +196,10 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
}
mBrightnessObserver.stopObserving();
+ mDisplayTracker.removeCallback(mBrightnessListener);
mUserTracker.removeCallback(mUserChangedCallback);
- mHandler.sendEmptyMessage(MSG_DETACH_LISTENER);
+ mMainHandler.sendEmptyMessage(MSG_DETACH_LISTENER);
}
};
@@ -225,7 +235,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
mBrightnessMin = info.brightnessMinimum;
// Value is passed as intbits, since this is what the message takes.
final int valueAsIntBits = Float.floatToIntBits(info.brightness);
- mHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits,
+ mMainHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits,
inVrMode ? 1 : 0).sendToTarget();
}
};
@@ -233,14 +243,14 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
@Override
public void onVrStateChanged(boolean enabled) {
- mHandler.obtainMessage(MSG_VR_MODE_CHANGED, enabled ? 1 : 0, 0)
+ mMainHandler.obtainMessage(MSG_VR_MODE_CHANGED, enabled ? 1 : 0, 0)
.sendToTarget();
}
};
- private final Handler mHandler = new Handler() {
+ private final Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
- public void handleMessage(Message msg) {
+ public boolean handleMessage(Message msg) {
mExternalChange = true;
try {
switch (msg.what) {
@@ -257,14 +267,18 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
updateVrMode(msg.arg1 != 0);
break;
default:
- super.handleMessage(msg);
+ return false;
+
}
} finally {
mExternalChange = false;
}
+ return true;
}
};
+ private final Handler mMainHandler;
+
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@Override
@@ -274,12 +288,17 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
}
};
+ @AssistedInject
public BrightnessController(
Context context,
- ToggleSlider control,
+ @Assisted ToggleSlider control,
UserTracker userTracker,
DisplayTracker displayTracker,
+ DisplayManager displayManager,
+ SecureSettings secureSettings,
+ @Nullable IVrManager iVrManager,
@Main Executor mainExecutor,
+ @Main Looper mainLooper,
@Background Handler bgHandler) {
mContext = context;
mControl = control;
@@ -288,22 +307,23 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
mBackgroundHandler = bgHandler;
mUserTracker = userTracker;
mDisplayTracker = displayTracker;
- mBrightnessObserver = new BrightnessObserver(mHandler);
-
+ mSecureSettings = secureSettings;
mDisplayId = mContext.getDisplayId();
- PowerManager pm = context.getSystemService(PowerManager.class);
+ mDisplayManager = displayManager;
+ mVrManager = iVrManager;
- mDisplayManager = context.getSystemService(DisplayManager.class);
- mVrManager = IVrManager.Stub.asInterface(ServiceManager.getService(
- Context.VR_SERVICE));
+ mMainHandler = new Handler(mainLooper, mHandlerCallback);
+ mBrightnessObserver = new BrightnessObserver(mMainHandler);
}
public void registerCallbacks() {
+ mBackgroundHandler.removeCallbacks(mStartListeningRunnable);
mBackgroundHandler.post(mStartListeningRunnable);
}
/** Unregister all call backs, both to and from the controller */
public void unregisterCallbacks() {
+ mBackgroundHandler.removeCallbacks(mStopListeningRunnable);
mBackgroundHandler.post(mStopListeningRunnable);
mControlValueInitialized = false;
}
@@ -418,38 +438,12 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig
mSliderAnimator.start();
}
- /** Factory for creating a {@link BrightnessController}. */
- public static class Factory {
- private final Context mContext;
- private final UserTracker mUserTracker;
- private final DisplayTracker mDisplayTracker;
- private final Executor mMainExecutor;
- private final Handler mBackgroundHandler;
-
- @Inject
- public Factory(
- Context context,
- UserTracker userTracker,
- DisplayTracker displayTracker,
- @Main Executor mainExecutor,
- @Background Handler bgHandler) {
- mContext = context;
- mUserTracker = userTracker;
- mDisplayTracker = displayTracker;
- mMainExecutor = mainExecutor;
- mBackgroundHandler = bgHandler;
- }
+
+ /** Factory for creating a {@link BrightnessController}. */
+ @AssistedFactory
+ public interface Factory {
/** Create a {@link BrightnessController} */
- public BrightnessController create(ToggleSlider toggleSlider) {
- return new BrightnessController(
- mContext,
- toggleSlider,
- mUserTracker,
- mDisplayTracker,
- mMainExecutor,
- mBackgroundHandler);
- }
+ BrightnessController create(ToggleSlider toggleSlider);
}
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 182e4569b549..38b1f14e45de 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -23,7 +23,6 @@ import static android.view.WindowManagerPolicyConstants.EXTRA_FROM_BRIGHTNESS_KE
import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
-import android.os.Handler;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
@@ -37,10 +36,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.settings.DisplayTracker;
-import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -56,26 +52,21 @@ public class BrightnessDialog extends Activity {
private BrightnessController mBrightnessController;
private final BrightnessSliderController.Factory mToggleSliderFactory;
- private final UserTracker mUserTracker;
- private final DisplayTracker mDisplayTracker;
+ private final BrightnessController.Factory mBrightnessControllerFactory;
private final DelayableExecutor mMainExecutor;
- private final Handler mBackgroundHandler;
private final AccessibilityManagerWrapper mAccessibilityMgr;
private Runnable mCancelTimeoutRunnable;
@Inject
public BrightnessDialog(
- UserTracker userTracker,
- DisplayTracker displayTracker,
- BrightnessSliderController.Factory factory,
+ BrightnessSliderController.Factory brightnessSliderfactory,
+ BrightnessController.Factory brightnessControllerFactory,
@Main DelayableExecutor mainExecutor,
- @Background Handler bgHandler,
- AccessibilityManagerWrapper accessibilityMgr) {
- mUserTracker = userTracker;
- mDisplayTracker = displayTracker;
- mToggleSliderFactory = factory;
+ AccessibilityManagerWrapper accessibilityMgr
+ ) {
+ mToggleSliderFactory = brightnessSliderfactory;
+ mBrightnessControllerFactory = brightnessControllerFactory;
mMainExecutor = mainExecutor;
- mBackgroundHandler = bgHandler;
mAccessibilityMgr = accessibilityMgr;
}
@@ -121,8 +112,7 @@ public class BrightnessDialog extends Activity {
controller.init();
frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT);
- mBrightnessController = new BrightnessController(
- this, controller, mUserTracker, mDisplayTracker, mMainExecutor, mBackgroundHandler);
+ mBrightnessController = mBrightnessControllerFactory.create(controller);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt
new file mode 100644
index 000000000000..45fc68a793bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.os.PowerManager
+import android.view.GestureDetector
+import android.view.MotionEvent
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import javax.inject.Inject
+
+/**
+ * This gestureListener will wake up by tap when the device is dreaming but not dozing, and the
+ * selected screensaver is hosted in lockscreen. Tap is gated by the falsing manager.
+ *
+ * Touches go through the [NotificationShadeWindowViewController].
+ */
+@SysUISingleton
+class LockscreenHostedDreamGestureListener
+@Inject
+constructor(
+ private val falsingManager: FalsingManager,
+ private val powerInteractor: PowerInteractor,
+ private val statusBarStateController: StatusBarStateController,
+ private val primaryBouncerInteractor: PrimaryBouncerInteractor,
+ private val keyguardRepository: KeyguardRepository,
+ private val shadeLogger: ShadeLogger,
+) : GestureDetector.SimpleOnGestureListener() {
+ private val TAG = this::class.simpleName
+
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ if (shouldHandleMotionEvent()) {
+ if (!falsingManager.isFalseTap(LOW_PENALTY)) {
+ shadeLogger.d("$TAG#onSingleTapUp tap handled, requesting wakeUpIfDreaming")
+ powerInteractor.wakeUpIfDreaming(
+ "DREAMING_SINGLE_TAP",
+ PowerManager.WAKE_REASON_TAP
+ )
+ } else {
+ shadeLogger.d("$TAG#onSingleTapUp false tap ignored")
+ }
+ return true
+ }
+ return false
+ }
+
+ private fun shouldHandleMotionEvent(): Boolean {
+ return keyguardRepository.isActiveDreamLockscreenHosted.value &&
+ statusBarStateController.state == StatusBarState.KEYGUARD &&
+ !primaryBouncerInteractor.isBouncerShowing()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index cfecf7de3780..ed7cbffc880b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -28,6 +28,7 @@ import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK;
import static com.android.systemui.classifier.Classifier.GENERIC;
import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
import static com.android.systemui.classifier.Classifier.UNLOCK;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -70,6 +71,7 @@ import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.MathUtils;
+import android.view.HapticFeedbackConstants;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -147,7 +149,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
@@ -168,7 +169,6 @@ import com.android.systemui.statusbar.NotificationShadeDepthController;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.NotificationShelfController;
import com.android.systemui.statusbar.PulseExpansionHandler;
-import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.VibratorHelper;
@@ -224,6 +224,8 @@ import com.android.systemui.util.Utils;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.animation.FlingAnimationUtils;
+import kotlin.Unit;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -234,8 +236,6 @@ import java.util.function.Consumer;
import javax.inject.Inject;
import javax.inject.Provider;
-import kotlin.Unit;
-
import kotlinx.coroutines.CoroutineDispatcher;
@SysUISingleton
@@ -366,7 +366,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private KeyguardBottomAreaView mKeyguardBottomArea;
private boolean mExpanding;
private boolean mSplitShadeEnabled;
- private boolean mDualShadeEnabled;
/** The bottom padding reserved for elements of the keyguard measuring notifications. */
private float mKeyguardNotificationBottomPadding;
/**
@@ -440,8 +439,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final FalsingCollector mFalsingCollector;
private final ShadeHeadsUpTrackerImpl mShadeHeadsUpTracker = new ShadeHeadsUpTrackerImpl();
private final ShadeFoldAnimator mShadeFoldAnimator = new ShadeFoldAnimatorImpl();
- private final ShadeNotificationPresenterImpl mShadeNotificationPresenter =
- new ShadeNotificationPresenterImpl();
private boolean mShowIconsWhenExpanded;
private int mIndicationBottomPadding;
@@ -602,7 +599,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final KeyguardInteractor mKeyguardInteractor;
private final KeyguardViewConfigurator mKeyguardViewConfigurator;
- private final @Nullable MultiShadeInteractor mMultiShadeInteractor;
private final CoroutineDispatcher mMainDispatcher;
private boolean mIsAnyMultiShadeExpanded;
private boolean mIsOcclusionTransitionRunning = false;
@@ -738,7 +734,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
- Provider<MultiShadeInteractor> multiShadeInteractorProvider,
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
KeyguardInteractor keyguardInteractor,
@@ -842,8 +837,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mFeatureFlags = featureFlags;
mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE);
mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES);
- mDualShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE);
- mMultiShadeInteractor = mDualShadeEnabled ? multiShadeInteractorProvider.get() : null;
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
@@ -1082,11 +1075,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
- if (mDualShadeEnabled) {
- collectFlow(mView, mMultiShadeInteractor.isAnyShadeExpanded(),
- mMultiShadeExpansionConsumer, mMainDispatcher);
- }
-
// Dreaming->Lockscreen
collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
mDreamingToLockscreenTransition, mMainDispatcher);
@@ -1172,6 +1160,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private void updateViewControllers(KeyguardStatusView keyguardStatusView,
FrameLayout userAvatarView,
KeyguardUserSwitcherView keyguardUserSwitcherView) {
+ if (mKeyguardStatusViewController != null) {
+ mKeyguardStatusViewController.onDestroy();
+ }
// Re-associate the KeyguardStatusViewController
KeyguardStatusViewComponent statusViewComponent =
mKeyguardStatusViewComponentFactory.build(keyguardStatusView);
@@ -2643,12 +2634,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
}
if (!mStatusBarStateController.isDozing()) {
- mVibratorHelper.vibrate(
- Process.myUid(),
- mView.getContext().getPackageName(),
- ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
- "falsing-additional-tap-required",
- TOUCH_VIBRATION_ATTRIBUTES);
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
+ } else {
+ mVibratorHelper.vibrate(
+ Process.myUid(),
+ mView.getContext().getPackageName(),
+ ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
+ "falsing-additional-tap-required",
+ TOUCH_VIBRATION_ATTRIBUTES);
+ }
}
}
@@ -3323,23 +3318,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
).printTableData(ipw);
}
- private final class ShadeNotificationPresenterImpl implements ShadeNotificationPresenter{
- @Override
- public RemoteInputController.Delegate createRemoteInputDelegate() {
- return mNotificationStackScrollLayoutController.createDelegate();
- }
-
- @Override
- public boolean hasPulsingNotifications() {
- return mNotificationListContainer.hasPulsingNotifications();
- }
- }
-
- @Override
- public ShadeNotificationPresenter getShadeNotificationPresenter() {
- return mShadeNotificationPresenter;
- }
-
@Override
public void initDependencies(
CentralSurfaces centralSurfaces,
@@ -3532,7 +3510,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private void maybeVibrateOnOpening(boolean openingWithTouch) {
if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) {
if (!openingWithTouch || !mHasVibratedOnOpen) {
- mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mView,
+ HapticFeedbackConstants.GESTURE_START
+ );
+ } else {
+ mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+ }
mHasVibratedOnOpen = true;
mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 481da52635f0..1f401fbbe6c1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -543,7 +543,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
state.forceUserActivity,
state.launchingActivityFromNotification,
state.mediaBackdropShowing,
- state.wallpaperSupportsAmbientMode,
state.windowNotTouchable,
state.componentsForcingTopUi,
state.forceOpenTokens,
@@ -734,12 +733,6 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
apply(mCurrentState);
}
- @Override
- public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
- mCurrentState.wallpaperSupportsAmbientMode = supportsAmbientMode;
- apply(mCurrentState);
- }
-
/**
* @param state The {@link StatusBarStateController} of the status bar.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index 7812f07fc59c..d25294343d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -46,7 +46,6 @@ class NotificationShadeWindowState(
@JvmField var forceUserActivity: Boolean = false,
@JvmField var launchingActivityFromNotification: Boolean = false,
@JvmField var mediaBackdropShowing: Boolean = false,
- @JvmField var wallpaperSupportsAmbientMode: Boolean = false,
@JvmField var windowNotTouchable: Boolean = false,
@JvmField var componentsForcingTopUi: MutableSet<String> = mutableSetOf(),
@JvmField var forceOpenTokens: MutableSet<Any> = mutableSetOf(),
@@ -84,7 +83,6 @@ class NotificationShadeWindowState(
forceUserActivity.toString(),
launchingActivityFromNotification.toString(),
mediaBackdropShowing.toString(),
- wallpaperSupportsAmbientMode.toString(),
windowNotTouchable.toString(),
componentsForcingTopUi.toString(),
forceOpenTokens.toString(),
@@ -124,7 +122,6 @@ class NotificationShadeWindowState(
forceUserActivity: Boolean,
launchingActivity: Boolean,
backdropShowing: Boolean,
- wallpaperSupportsAmbientMode: Boolean,
notTouchable: Boolean,
componentsForcingTopUi: MutableSet<String>,
forceOpenTokens: MutableSet<Any>,
@@ -153,7 +150,6 @@ class NotificationShadeWindowState(
this.forceUserActivity = forceUserActivity
this.launchingActivityFromNotification = launchingActivity
this.mediaBackdropShowing = backdropShowing
- this.wallpaperSupportsAmbientMode = wallpaperSupportsAmbientMode
this.windowNotTouchable = notTouchable
this.componentsForcingTopUi.clear()
this.componentsForcingTopUi.addAll(componentsForcingTopUi)
@@ -200,7 +196,6 @@ class NotificationShadeWindowState(
"forceUserActivity",
"launchingActivity",
"backdropShowing",
- "wallpaperSupportsAmbientMode",
"notTouchable",
"componentsForcingTopUi",
"forceOpenTokens",
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index f6db9e434610..6afed1d76918 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -16,6 +16,7 @@
package com.android.systemui.shade;
+import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
@@ -30,9 +31,6 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewStub;
-
-import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
@@ -45,7 +43,6 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder;
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dock.DockManager;
import com.android.systemui.flags.FeatureFlags;
@@ -56,9 +53,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.log.BouncerLogger;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
-import com.android.systemui.multishade.ui.view.MultiShadeView;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.DragDownHelper;
@@ -82,7 +76,6 @@ import java.util.Optional;
import java.util.function.Consumer;
import javax.inject.Inject;
-import javax.inject.Provider;
/**
* Controller for {@link NotificationShadeWindowView}.
@@ -102,9 +95,12 @@ public class NotificationShadeWindowViewController {
private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
private final AmbientState mAmbientState;
private final PulsingGestureListener mPulsingGestureListener;
+ private final LockscreenHostedDreamGestureListener mLockscreenHostedDreamGestureListener;
private final NotificationInsetsController mNotificationInsetsController;
private final boolean mIsTrackpadCommonEnabled;
+ private final FeatureFlags mFeatureFlags;
private GestureDetector mPulsingWakeupGestureHandler;
+ private GestureDetector mDreamingWakeupGestureHandler;
private View mBrightnessMirror;
private boolean mTouchActive;
private boolean mTouchCancelled;
@@ -131,7 +127,6 @@ public class NotificationShadeWindowViewController {
step.getTransitionState() == TransitionState.RUNNING;
};
private final SystemClock mClock;
- private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor;
@Inject
public NotificationShadeWindowViewController(
@@ -156,15 +151,14 @@ public class NotificationShadeWindowViewController {
NotificationInsetsController notificationInsetsController,
AmbientState ambientState,
PulsingGestureListener pulsingGestureListener,
+ LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener,
KeyguardBouncerViewModel keyguardBouncerViewModel,
KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory,
KeyguardMessageAreaController.Factory messageAreaControllerFactory,
KeyguardTransitionInteractor keyguardTransitionInteractor,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
FeatureFlags featureFlags,
- Provider<MultiShadeInteractor> multiShadeInteractorProvider,
SystemClock clock,
- Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
BouncerMessageInteractor bouncerMessageInteractor,
BouncerLogger bouncerLogger) {
mLockscreenShadeTransitionController = transitionController;
@@ -187,8 +181,10 @@ public class NotificationShadeWindowViewController {
mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
mAmbientState = ambientState;
mPulsingGestureListener = pulsingGestureListener;
+ mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener;
mNotificationInsetsController = notificationInsetsController;
mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON);
+ mFeatureFlags = featureFlags;
// This view is not part of the newly inflated expanded status bar.
mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
@@ -212,17 +208,6 @@ public class NotificationShadeWindowViewController {
progressProvider -> progressProvider.addCallback(
mDisableSubpixelTextTransitionListener));
}
- if (ComposeFacade.INSTANCE.isComposeAvailable()
- && featureFlags.isEnabled(Flags.DUAL_SHADE)) {
- mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get();
- final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub);
- if (multiShadeViewStub != null) {
- final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate();
- multiShadeView.init(multiShadeInteractorProvider.get(), clock);
- }
- } else {
- mMultiShadeMotionEventInteractor = null;
- }
}
/**
@@ -237,7 +222,10 @@ public class NotificationShadeWindowViewController {
mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(),
mPulsingGestureListener);
-
+ if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) {
+ mDreamingWakeupGestureHandler = new GestureDetector(mView.getContext(),
+ mLockscreenHostedDreamGestureListener);
+ }
mView.setLayoutInsetsController(mNotificationInsetsController);
mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
@Override
@@ -291,6 +279,10 @@ public class NotificationShadeWindowViewController {
mFalsingCollector.onTouchEvent(ev);
mPulsingWakeupGestureHandler.onTouchEvent(ev);
+ if (mDreamingWakeupGestureHandler != null
+ && mDreamingWakeupGestureHandler.onTouchEvent(ev)) {
+ return true;
+ }
if (mStatusBarKeyguardViewManager.dispatchTouchEvent(ev)) {
return true;
}
@@ -381,10 +373,7 @@ public class NotificationShadeWindowViewController {
return true;
}
- if (mMultiShadeMotionEventInteractor != null) {
- // This interactor is not null only if the dual shade feature is enabled.
- return mMultiShadeMotionEventInteractor.shouldIntercept(ev);
- } else if (mNotificationPanelViewController.isFullyExpanded()
+ if (mNotificationPanelViewController.isFullyExpanded()
&& mDragDownHelper.isDragDownEnabled()
&& !mService.isBouncerShowing()
&& !mStatusBarStateController.isDozing()) {
@@ -414,10 +403,7 @@ public class NotificationShadeWindowViewController {
return true;
}
- if (mMultiShadeMotionEventInteractor != null) {
- // This interactor is not null only if the dual shade feature is enabled.
- return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth());
- } else if (mDragDownHelper.isDragDownEnabled()
+ if (mDragDownHelper.isDragDownEnabled()
|| mDragDownHelper.isDraggingDown()) {
// we still want to finish our drag down gesture when locking the screen
return mDragDownHelper.onTouchEvent(ev) || handled;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index ee4e98e094fc..fe4832f0895b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
+import android.graphics.Point
import android.hardware.display.AmbientDisplayConfiguration
import android.os.PowerManager
import android.provider.Settings
@@ -25,6 +26,7 @@ import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.DozeInteractor
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -52,6 +54,7 @@ class PulsingGestureListener @Inject constructor(
private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
private val statusBarStateController: StatusBarStateController,
private val shadeLogger: ShadeLogger,
+ private val dozeInteractor: DozeInteractor,
userTracker: UserTracker,
tunerService: TunerService,
dumpManager: DumpManager
@@ -86,6 +89,7 @@ class PulsingGestureListener @Inject constructor(
shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
if (proximityIsNotNear && isNotAFalseTap) {
shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
+ dozeInteractor.setLastTapToWakePosition(Point(e.x.toInt(), e.y.toInt()))
powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP)
}
return true
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
new file mode 100644
index 000000000000..7a803867d4a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+
+/** Fulfills dependencies on the shade with empty implementations for variants with no shade. */
+@Module
+abstract class ShadeEmptyImplModule {
+ @Binds
+ @SysUISingleton
+ abstract fun bindsShadeViewController(svc: ShadeViewControllerEmptyImpl): ShadeViewController
+
+ @Binds
+ @SysUISingleton
+ abstract fun bindsShadeController(sc: ShadeControllerEmptyImpl): ShadeController
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 8b89ff49f418..529f12e0658e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -54,7 +54,7 @@ import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_H
import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_TRANSITION_ID
import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT
import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
-import com.android.systemui.shade.ShadeModule.Companion.SHADE_HEADER
+import com.android.systemui.shade.ShadeViewProviderModule.Companion.SHADE_HEADER
import com.android.systemui.shade.carrier.ShadeCarrierGroup
import com.android.systemui.shade.carrier.ShadeCarrierGroupController
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 3c4ad7222576..6a332dd164af 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -16,311 +16,20 @@
package com.android.systemui.shade
-import android.annotation.SuppressLint
-import android.content.ContentResolver
-import android.os.Handler
-import android.view.LayoutInflater
-import android.view.ViewStub
-import androidx.constraintlayout.motion.widget.MotionLayout
-import com.android.keyguard.LockIconView
-import com.android.systemui.CoreStartable
-import com.android.systemui.R
-import com.android.systemui.battery.BatteryMeterView
-import com.android.systemui.battery.BatteryMeterViewController
-import com.android.systemui.biometrics.AuthRippleController
-import com.android.systemui.biometrics.AuthRippleView
-import com.android.systemui.compose.ComposeFacade
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.ui.view.KeyguardRootView
-import com.android.systemui.privacy.OngoingPrivacyChip
-import com.android.systemui.scene.shared.model.Scene
-import com.android.systemui.scene.shared.model.SceneContainerConfig
-import com.android.systemui.scene.shared.model.SceneContainerNames
-import com.android.systemui.scene.ui.view.SceneWindowRootView
-import com.android.systemui.scene.ui.view.WindowRootView
-import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.statusbar.LightRevealScrim
-import com.android.systemui.statusbar.NotificationShelf
-import com.android.systemui.statusbar.NotificationShelfController
-import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent
-import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
-import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
-import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.phone.StatusIconContainer
-import com.android.systemui.statusbar.phone.TapAgainView
-import com.android.systemui.statusbar.policy.BatteryController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.tuner.TunerService
import dagger.Binds
import dagger.Module
-import dagger.Provides
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import javax.inject.Named
-import javax.inject.Provider
/** Module for classes related to the notification shade. */
-@Module(includes = [StartShadeModule::class])
+@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class])
abstract class ShadeModule {
-
- @Binds
- @IntoMap
- @ClassKey(AuthRippleController::class)
- abstract fun bindAuthRippleController(controller: AuthRippleController): CoreStartable
-
@Binds
@SysUISingleton
abstract fun bindsShadeViewController(
notificationPanelViewController: NotificationPanelViewController
): ShadeViewController
- companion object {
- const val SHADE_HEADER = "large_screen_shade_header"
-
- @SuppressLint("InflateParams") // Root views don't have parents.
- @Provides
- @SysUISingleton
- fun providesWindowRootView(
- layoutInflater: LayoutInflater,
- featureFlags: FeatureFlags,
- @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
- viewModelProvider: Provider<SceneContainerViewModel>,
- @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
- containerConfigProvider: Provider<SceneContainerConfig>,
- @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
- scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
- ): WindowRootView {
- return if (
- featureFlags.isEnabled(Flags.SCENE_CONTAINER) && ComposeFacade.isComposeAvailable()
- ) {
- val sceneWindowRootView =
- layoutInflater.inflate(R.layout.scene_window_root, null) as SceneWindowRootView
- sceneWindowRootView.init(
- viewModel = viewModelProvider.get(),
- containerConfig = containerConfigProvider.get(),
- scenes = scenesProvider.get(),
- )
- sceneWindowRootView
- } else {
- layoutInflater.inflate(R.layout.super_notification_shade, null)
- }
- as WindowRootView?
- ?: throw IllegalStateException("Window root view could not be properly inflated")
- }
-
- @Provides
- @SysUISingleton
- // TODO(b/277762009): Do something similar to
- // {@link StatusBarWindowModule.InternalWindowView} so that only
- // {@link NotificationShadeWindowViewController} can inject this view.
- fun providesNotificationShadeWindowView(
- root: WindowRootView,
- featureFlags: FeatureFlags,
- ): NotificationShadeWindowView {
- if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
- return root.findViewById(R.id.legacy_window_root)
- }
- return root as NotificationShadeWindowView?
- ?: throw IllegalStateException("root view not a NotificationShadeWindowView")
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- fun providesNotificationStackScrollLayout(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): NotificationStackScrollLayout {
- return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller)
- }
-
- @Provides
- @SysUISingleton
- fun providesNotificationShelfController(
- featureFlags: FeatureFlags,
- newImpl: Provider<NotificationShelfViewBinderWrapperControllerImpl>,
- notificationShelfComponentBuilder: NotificationShelfComponent.Builder,
- layoutInflater: LayoutInflater,
- notificationStackScrollLayout: NotificationStackScrollLayout,
- ): NotificationShelfController {
- return if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
- newImpl.get()
- } else {
- val shelfView =
- layoutInflater.inflate(
- R.layout.status_bar_notification_shelf,
- notificationStackScrollLayout,
- false
- ) as NotificationShelf
- val component =
- notificationShelfComponentBuilder.notificationShelf(shelfView).build()
- val notificationShelfController = component.notificationShelfController
- notificationShelfController.init()
- notificationShelfController
- }
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- fun providesNotificationPanelView(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): NotificationPanelView {
- return notificationShadeWindowView.findViewById(R.id.notification_panel)
- }
-
- /**
- * Constructs a new, unattached [KeyguardBottomAreaView].
- *
- * Note that this is explicitly _not_ a singleton, as we want to be able to reinflate it
- */
- @Provides
- fun providesKeyguardBottomAreaView(
- npv: NotificationPanelView,
- layoutInflater: LayoutInflater,
- ): KeyguardBottomAreaView {
- return layoutInflater.inflate(R.layout.keyguard_bottom_area, npv, false)
- as KeyguardBottomAreaView
- }
-
- @Provides
- @SysUISingleton
- fun providesLightRevealScrim(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): LightRevealScrim {
- return notificationShadeWindowView.findViewById(R.id.light_reveal_scrim)
- }
-
- @Provides
- @SysUISingleton
- fun providesKeyguardRootView(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): KeyguardRootView {
- return notificationShadeWindowView.findViewById(R.id.keyguard_root_view)
- }
-
- @Provides
- @SysUISingleton
- fun providesSharedNotificationContainer(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): SharedNotificationContainer {
- return notificationShadeWindowView.findViewById(R.id.shared_notification_container)
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- fun providesAuthRippleView(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): AuthRippleView? {
- return notificationShadeWindowView.findViewById(R.id.auth_ripple)
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- fun providesLockIconView(
- keyguardRootView: KeyguardRootView,
- notificationPanelView: NotificationPanelView,
- featureFlags: FeatureFlags
- ): LockIconView {
- if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
- return keyguardRootView.findViewById(R.id.lock_icon_view)
- } else {
- return notificationPanelView.findViewById(R.id.lock_icon_view)
- }
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- fun providesTapAgainView(
- notificationPanelView: NotificationPanelView,
- ): TapAgainView {
- return notificationPanelView.findViewById(R.id.shade_falsing_tap_again)
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- fun providesNotificationsQuickSettingsContainer(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): NotificationsQuickSettingsContainer {
- return notificationShadeWindowView.findViewById(R.id.notification_container_parent)
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- @Named(SHADE_HEADER)
- fun providesShadeHeaderView(
- notificationShadeWindowView: NotificationShadeWindowView,
- ): MotionLayout {
- val stub = notificationShadeWindowView.findViewById<ViewStub>(R.id.qs_header_stub)
- val layoutId = R.layout.combined_qs_header
- stub.layoutResource = layoutId
- return stub.inflate() as MotionLayout
- }
-
- @Provides
- @SysUISingleton
- fun providesCombinedShadeHeadersConstraintManager(): CombinedShadeHeadersConstraintManager {
- return CombinedShadeHeadersConstraintManagerImpl
- }
-
- // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
- @Provides
- @SysUISingleton
- @Named(SHADE_HEADER)
- fun providesBatteryMeterView(@Named(SHADE_HEADER) view: MotionLayout): BatteryMeterView {
- return view.findViewById(R.id.batteryRemainingIcon)
- }
-
- @Provides
- @SysUISingleton
- @Named(SHADE_HEADER)
- fun providesBatteryMeterViewController(
- @Named(SHADE_HEADER) batteryMeterView: BatteryMeterView,
- userTracker: UserTracker,
- configurationController: ConfigurationController,
- tunerService: TunerService,
- @Main mainHandler: Handler,
- contentResolver: ContentResolver,
- batteryController: BatteryController,
- ): BatteryMeterViewController {
- return BatteryMeterViewController(
- batteryMeterView,
- StatusBarLocation.QS,
- userTracker,
- configurationController,
- tunerService,
- mainHandler,
- contentResolver,
- batteryController,
- )
- }
-
- @Provides
- @SysUISingleton
- @Named(SHADE_HEADER)
- fun providesOngoingPrivacyChip(
- @Named(SHADE_HEADER) header: MotionLayout,
- ): OngoingPrivacyChip {
- return header.findViewById(R.id.privacy_chip)
- }
-
- @Provides
- @SysUISingleton
- @Named(SHADE_HEADER)
- fun providesStatusIconContainer(
- @Named(SHADE_HEADER) header: MotionLayout,
- ): StatusIconContainer {
- return header.findViewById(R.id.statusIcons)
- }
- }
+ @Binds
+ @SysUISingleton
+ abstract fun bindsShadeController(shadeControllerImpl: ShadeControllerImpl): ShadeController
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
index 8d5c30b51677..2532bad1d7a7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
@@ -18,6 +18,7 @@ package com.android.systemui.shade
import android.view.ViewPropertyAnimator
import com.android.systemui.statusbar.GestureRecorder
import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
@@ -63,6 +64,9 @@ interface ShadeSurface : ShadeViewController {
/** Animates the view from its current alpha to zero then runs the runnable. */
fun fadeOut(startDelayMs: Long, durationMs: Long, endAction: Runnable): ViewPropertyAnimator
+ /** Returns the NSSL controller. */
+ val notificationStackScrollLayoutController: NotificationStackScrollLayoutController
+
/** Set whether the bouncer is showing. */
fun setBouncerShowing(bouncerShowing: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index 9aa5eb0cd68b..d5b5c87ec781 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -19,9 +19,7 @@ import android.view.MotionEvent
import android.view.ViewGroup
import android.view.ViewTreeObserver
import com.android.systemui.keyguard.shared.model.WakefulnessModel
-import com.android.systemui.statusbar.RemoteInputController
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController
import com.android.systemui.statusbar.phone.KeyguardStatusBarView
import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController
@@ -141,9 +139,6 @@ interface ShadeViewController {
/** Returns the StatusBarState. */
val barState: Int
- /** Returns the NSSL controller. */
- val notificationStackScrollLayoutController: NotificationStackScrollLayoutController
-
/** Sets the amount of progress in the status bar launch animation. */
fun applyLaunchAnimationProgress(linearProgress: Float)
@@ -261,9 +256,6 @@ interface ShadeViewController {
/** Returns the ShadeFoldAnimator. */
val shadeFoldAnimator: ShadeFoldAnimator
- /** Returns the ShadeNotificationPresenter. */
- val shadeNotificationPresenter: ShadeNotificationPresenter
-
companion object {
/**
* Returns a multiplicative factor to use when determining the falsing threshold for touches
@@ -325,16 +317,7 @@ interface ShadeFoldAnimator {
fun cancelFoldToAodAnimation()
/** Returns the main view of the shade. */
- val view: ViewGroup
-}
-
-/** Handles the shade's interactions with StatusBarNotificationPresenter. */
-interface ShadeNotificationPresenter {
- /** Returns a new delegate for some view controller pieces of the remote input process. */
- fun createRemoteInputDelegate(): RemoteInputController.Delegate
-
- /** Returns whether the screen has temporarily woken up to display notifications. */
- fun hasPulsingNotifications(): Boolean
+ val view: ViewGroup?
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
new file mode 100644
index 000000000000..287ac528385f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.view.MotionEvent
+import android.view.ViewGroup
+import android.view.ViewTreeObserver
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.phone.HeadsUpAppearanceController
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/** Empty implementation of ShadeViewController for variants with no shade. */
+class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeViewController {
+ override fun expand(animate: Boolean) {}
+ override fun expandToQs() {}
+ override fun expandToNotifications() {}
+ override val isExpandingOrCollapsing: Boolean = false
+ override val isExpanded: Boolean = false
+ override val isPanelExpanded: Boolean = false
+ override val isShadeFullyExpanded: Boolean = false
+ override fun collapse(delayed: Boolean, speedUpFactor: Float) {}
+ override fun collapse(animate: Boolean, delayed: Boolean, speedUpFactor: Float) {}
+ override fun collapseWithDuration(animationDuration: Int) {}
+ override fun instantCollapse() {}
+ override fun animateCollapseQs(fullyCollapse: Boolean) {}
+ override fun canBeCollapsed(): Boolean = false
+ override val isCollapsing: Boolean = false
+ override val isFullyCollapsed: Boolean = false
+ override val isTracking: Boolean = false
+ override val isViewEnabled: Boolean = false
+ override fun setOpenCloseListener(openCloseListener: OpenCloseListener) {}
+ override fun shouldHideStatusBarIconsWhenExpanded() = false
+ override fun blockExpansionForCurrentTouch() {}
+ override fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener) {}
+ override fun disableHeader(state1: Int, state2: Int, animated: Boolean) {}
+ override fun startExpandLatencyTracking() {}
+ override fun startBouncerPreHideAnimation() {}
+ override fun dozeTimeTick() {}
+ override fun resetViews(animate: Boolean) {}
+ override val barState: Int = 0
+ override fun applyLaunchAnimationProgress(linearProgress: Float) {}
+ override fun closeUserSwitcherIfOpen(): Boolean {
+ return false
+ }
+ override fun onBackPressed() {}
+ override fun setIsLaunchAnimationRunning(running: Boolean) {}
+ override fun setAlpha(alpha: Int, animate: Boolean) {}
+ override fun setAlphaChangeAnimationEndAction(r: Runnable) {}
+ override fun setPulsing(pulsing: Boolean) {}
+ override fun setQsScrimEnabled(qsScrimEnabled: Boolean) {}
+ override fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean) {}
+ override fun updateSystemUiStateFlags() {}
+ override fun updateTouchableRegion() {}
+ override fun addOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener) {}
+ override fun removeOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener) {}
+ override fun postToView(action: Runnable): Boolean {
+ return false
+ }
+ override fun transitionToExpandedShade(delay: Long) {}
+ override val isUnlockHintRunning: Boolean = false
+
+ override fun resetViewGroupFade() {}
+ override fun setKeyguardTransitionProgress(keyguardAlpha: Float, keyguardTranslationY: Int) {}
+ override fun setOverStretchAmount(amount: Float) {}
+ override fun setKeyguardStatusBarAlpha(alpha: Float) {}
+ override fun showAodUi() {}
+ override fun isFullyExpanded(): Boolean {
+ return false
+ }
+ override fun handleExternalTouch(event: MotionEvent): Boolean {
+ return false
+ }
+ override fun startTrackingExpansionFromStatusBar() {}
+ override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl()
+ override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl()
+}
+
+class ShadeHeadsUpTrackerEmptyImpl : ShadeHeadsUpTracker {
+ override fun addTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {}
+ override fun removeTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {}
+ override fun setHeadsUpAppearanceController(
+ headsUpAppearanceController: HeadsUpAppearanceController?
+ ) {}
+ override val trackedHeadsUpNotification: ExpandableNotificationRow? = null
+}
+
+class ShadeFoldAnimatorEmptyImpl : ShadeFoldAnimator {
+ override fun prepareFoldToAodAnimation() {}
+ override fun startFoldToAodAnimation(
+ startAction: Runnable,
+ endAction: Runnable,
+ cancelAction: Runnable,
+ ) {}
+ override fun cancelFoldToAodAnimation() {}
+ override val view: ViewGroup? = null
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
new file mode 100644
index 000000000000..fc6479eb62a4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.os.Handler
+import android.view.LayoutInflater
+import android.view.ViewStub
+import androidx.constraintlayout.motion.widget.MotionLayout
+import com.android.keyguard.LockIconView
+import com.android.systemui.R
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.biometrics.AuthRippleView
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.scene.shared.model.Scene
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.SceneContainerNames
+import com.android.systemui.scene.ui.view.SceneWindowRootView
+import com.android.systemui.scene.ui.view.WindowRootView
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.LightRevealScrim
+import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.NotificationShelfController
+import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent
+import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
+import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.phone.StatusIconContainer
+import com.android.systemui.statusbar.phone.TapAgainView
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.tuner.TunerService
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+import javax.inject.Provider
+
+/** Module for providing views related to the shade. */
+@Module
+abstract class ShadeViewProviderModule {
+ companion object {
+ const val SHADE_HEADER = "large_screen_shade_header"
+
+ @SuppressLint("InflateParams") // Root views don't have parents.
+ @Provides
+ @SysUISingleton
+ fun providesWindowRootView(
+ layoutInflater: LayoutInflater,
+ featureFlags: FeatureFlags,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ viewModelProvider: Provider<SceneContainerViewModel>,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ containerConfigProvider: Provider<SceneContainerConfig>,
+ @Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
+ ): WindowRootView {
+ return if (
+ featureFlags.isEnabled(Flags.SCENE_CONTAINER) && ComposeFacade.isComposeAvailable()
+ ) {
+ val sceneWindowRootView =
+ layoutInflater.inflate(R.layout.scene_window_root, null) as SceneWindowRootView
+ sceneWindowRootView.init(
+ viewModel = viewModelProvider.get(),
+ containerConfig = containerConfigProvider.get(),
+ scenes = scenesProvider.get(),
+ )
+ sceneWindowRootView
+ } else {
+ layoutInflater.inflate(R.layout.super_notification_shade, null)
+ }
+ as WindowRootView?
+ ?: throw IllegalStateException("Window root view could not be properly inflated")
+ }
+
+ @Provides
+ @SysUISingleton
+ // TODO(b/277762009): Do something similar to
+ // {@link StatusBarWindowModule.InternalWindowView} so that only
+ // {@link NotificationShadeWindowViewController} can inject this view.
+ fun providesNotificationShadeWindowView(
+ root: WindowRootView,
+ featureFlags: FeatureFlags,
+ ): NotificationShadeWindowView {
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ return root.findViewById(R.id.legacy_window_root)
+ }
+ return root as NotificationShadeWindowView?
+ ?: throw IllegalStateException("root view not a NotificationShadeWindowView")
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ fun providesNotificationStackScrollLayout(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): NotificationStackScrollLayout {
+ return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller)
+ }
+
+ @Provides
+ @SysUISingleton
+ fun providesNotificationShelfController(
+ featureFlags: FeatureFlags,
+ newImpl: Provider<NotificationShelfViewBinderWrapperControllerImpl>,
+ notificationShelfComponentBuilder: NotificationShelfComponent.Builder,
+ layoutInflater: LayoutInflater,
+ notificationStackScrollLayout: NotificationStackScrollLayout,
+ ): NotificationShelfController {
+ return if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+ newImpl.get()
+ } else {
+ val shelfView =
+ layoutInflater.inflate(
+ R.layout.status_bar_notification_shelf,
+ notificationStackScrollLayout,
+ false
+ ) as NotificationShelf
+ val component =
+ notificationShelfComponentBuilder.notificationShelf(shelfView).build()
+ val notificationShelfController = component.notificationShelfController
+ notificationShelfController.init()
+ notificationShelfController
+ }
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ fun providesNotificationPanelView(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): NotificationPanelView {
+ return notificationShadeWindowView.findViewById(R.id.notification_panel)
+ }
+
+ /**
+ * Constructs a new, unattached [KeyguardBottomAreaView].
+ *
+ * Note that this is explicitly _not_ a singleton, as we want to be able to reinflate it
+ */
+ @Provides
+ fun providesKeyguardBottomAreaView(
+ npv: NotificationPanelView,
+ layoutInflater: LayoutInflater,
+ ): KeyguardBottomAreaView {
+ return layoutInflater.inflate(R.layout.keyguard_bottom_area, npv, false)
+ as KeyguardBottomAreaView
+ }
+
+ @Provides
+ @SysUISingleton
+ fun providesLightRevealScrim(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): LightRevealScrim {
+ return notificationShadeWindowView.findViewById(R.id.light_reveal_scrim)
+ }
+
+ @Provides
+ @SysUISingleton
+ fun providesKeyguardRootView(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): KeyguardRootView {
+ return notificationShadeWindowView.findViewById(R.id.keyguard_root_view)
+ }
+
+ @Provides
+ @SysUISingleton
+ fun providesSharedNotificationContainer(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): SharedNotificationContainer {
+ return notificationShadeWindowView.findViewById(R.id.shared_notification_container)
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ fun providesAuthRippleView(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): AuthRippleView? {
+ return notificationShadeWindowView.findViewById(R.id.auth_ripple)
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ fun providesLockIconView(
+ keyguardRootView: KeyguardRootView,
+ notificationPanelView: NotificationPanelView,
+ featureFlags: FeatureFlags
+ ): LockIconView {
+ if (featureFlags.isEnabled(Flags.MIGRATE_LOCK_ICON)) {
+ return keyguardRootView.findViewById(R.id.lock_icon_view)
+ } else {
+ return notificationPanelView.findViewById(R.id.lock_icon_view)
+ }
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ fun providesTapAgainView(
+ notificationPanelView: NotificationPanelView,
+ ): TapAgainView {
+ return notificationPanelView.findViewById(R.id.shade_falsing_tap_again)
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ fun providesNotificationsQuickSettingsContainer(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): NotificationsQuickSettingsContainer {
+ return notificationShadeWindowView.findViewById(R.id.notification_container_parent)
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ @Named(SHADE_HEADER)
+ fun providesShadeHeaderView(
+ notificationShadeWindowView: NotificationShadeWindowView,
+ ): MotionLayout {
+ val stub = notificationShadeWindowView.findViewById<ViewStub>(R.id.qs_header_stub)
+ val layoutId = R.layout.combined_qs_header
+ stub.layoutResource = layoutId
+ return stub.inflate() as MotionLayout
+ }
+
+ @Provides
+ @SysUISingleton
+ fun providesCombinedShadeHeadersConstraintManager(): CombinedShadeHeadersConstraintManager {
+ return CombinedShadeHeadersConstraintManagerImpl
+ }
+
+ // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+ @Provides
+ @SysUISingleton
+ @Named(SHADE_HEADER)
+ fun providesBatteryMeterView(@Named(SHADE_HEADER) view: MotionLayout): BatteryMeterView {
+ return view.findViewById(R.id.batteryRemainingIcon)
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SHADE_HEADER)
+ fun providesBatteryMeterViewController(
+ @Named(SHADE_HEADER) batteryMeterView: BatteryMeterView,
+ userTracker: UserTracker,
+ configurationController: ConfigurationController,
+ tunerService: TunerService,
+ @Main mainHandler: Handler,
+ contentResolver: ContentResolver,
+ batteryController: BatteryController,
+ ): BatteryMeterViewController {
+ return BatteryMeterViewController(
+ batteryMeterView,
+ StatusBarLocation.QS,
+ userTracker,
+ configurationController,
+ tunerService,
+ mainHandler,
+ contentResolver,
+ batteryController,
+ )
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SHADE_HEADER)
+ fun providesOngoingPrivacyChip(
+ @Named(SHADE_HEADER) header: MotionLayout,
+ ): OngoingPrivacyChip {
+ return header.findViewById(R.id.privacy_chip)
+ }
+
+ @Provides
+ @SysUISingleton
+ @Named(SHADE_HEADER)
+ fun providesStatusIconContainer(
+ @Named(SHADE_HEADER) header: MotionLayout,
+ ): StatusIconContainer {
+ return header.findViewById(R.id.statusIcons)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
index c50693c30533..15ec18c528b6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/StartShadeModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import com.android.systemui.CoreStartable
+import com.android.systemui.biometrics.AuthRippleController
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
@@ -28,4 +29,9 @@ internal abstract class StartShadeModule {
@IntoMap
@ClassKey(ShadeController::class)
abstract fun bind(shadeController: ShadeController): CoreStartable
+
+ @Binds
+ @IntoMap
+ @ClassKey(AuthRippleController::class)
+ abstract fun bindAuthRippleController(controller: AuthRippleController): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
index 5d06f8d083d2..15ff31fe8c86 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt
@@ -21,11 +21,12 @@ import android.widget.FrameLayout
/**
* A temporary base class that's shared between our old status bar connectivity view implementations
- * ([StatusBarMobileView]) and our new status bar implementations ([ModernStatusBarWifiView],
- * [ModernStatusBarMobileView]).
+ * and our new status bar implementations ([ModernStatusBarWifiView], [ModernStatusBarMobileView]).
*
* Once our refactor is over, we should be able to delete this go-between class and the old view
* class.
+ *
+ * NOTE: the old classes are now deleted, and this class can be removed.
*/
abstract class BaseStatusBarFrameLayout
@JvmOverloads
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 92df78bac17f..6304c1ea2635 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -168,7 +168,10 @@ public class CommandQueue extends IStatusBar.Stub implements
private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT;
private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT;
private static final int MSG_TOGGLE_TASKBAR = 73 << MSG_SHIFT;
-
+ private static final int MSG_SETTING_CHANGED = 74 << MSG_SHIFT;
+ private static final int MSG_LOCK_TASK_MODE_CHANGED = 75 << MSG_SHIFT;
+ private static final int MSG_CONFIRM_IMMERSIVE_PROMPT = 77 << MSG_SHIFT;
+ private static final int MSG_IMMERSIVE_CHANGED = 78 << MSG_SHIFT;
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1;
@@ -498,6 +501,16 @@ public class CommandQueue extends IStatusBar.Stub implements
* @see IStatusBar#showMediaOutputSwitcher
*/
default void showMediaOutputSwitcher(String packageName) {}
+
+ /**
+ * @see IStatusBar#confirmImmersivePrompt
+ */
+ default void confirmImmersivePrompt() {}
+
+ /**
+ * @see IStatusBar#immersiveModeChanged
+ */
+ default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
}
@VisibleForTesting
@@ -783,6 +796,23 @@ public class CommandQueue extends IStatusBar.Stub implements
}
@Override
+ public void confirmImmersivePrompt() {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_CONFIRM_IMMERSIVE_PROMPT).sendToTarget();
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ synchronized (mLock) {
+ final SomeArgs args = SomeArgs.obtain();
+ args.argi1 = rootDisplayAreaId;
+ args.argi2 = isImmersiveMode ? 1 : 0;
+ mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
+ }
+ }
+
+ @Override
public void appTransitionPending(int displayId) {
appTransitionPending(displayId, false /* forced */);
}
@@ -1810,6 +1840,19 @@ public class CommandQueue extends IStatusBar.Stub implements
mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName);
}
break;
+ case MSG_CONFIRM_IMMERSIVE_PROMPT:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).confirmImmersivePrompt();
+ }
+ break;
+ case MSG_IMMERSIVE_CHANGED:
+ args = (SomeArgs) msg.obj;
+ int rootDisplayAreaId = args.argi1;
+ boolean isImmersiveMode = args.argi2 != 0;
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ }
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
new file mode 100644
index 000000000000..a7ec02ff43c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
+import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+import static android.app.StatusBarManager.DISABLE_BACK;
+import static android.app.StatusBarManager.DISABLE_HOME;
+import static android.app.StatusBarManager.DISABLE_RECENT;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
+import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
+import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
+
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.graphics.Insets;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowInsets.Type;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.R;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+import com.android.systemui.util.settings.SecureSettings;
+
+import javax.inject.Inject;
+
+/**
+ * Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
+ * entering immersive mode.
+ */
+public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Callbacks,
+ TaskStackChangeListener {
+ private static final String TAG = "ImmersiveModeConfirm";
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
+ private static final String CONFIRMED = "confirmed";
+ private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE =
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
+
+ private static boolean sConfirmed;
+ private final SecureSettings mSecureSettings;
+
+ private Context mDisplayContext;
+ private final Context mSysUiContext;
+ private final Handler mHandler = new H(Looper.getMainLooper());
+ private long mShowDelayMs = 0L;
+ private final IBinder mWindowToken = new Binder();
+ private final CommandQueue mCommandQueue;
+
+ private ClingWindowView mClingWindow;
+ /** The last {@link WindowManager} that is used to add the confirmation window. */
+ @Nullable
+ private WindowManager mWindowManager;
+ /**
+ * The WindowContext that is registered with {@link #mWindowManager} with options to specify the
+ * {@link RootDisplayArea} to attach the confirmation window.
+ */
+ @Nullable
+ private Context mWindowContext;
+ /**
+ * The root display area feature id that the {@link #mWindowContext} is attaching to.
+ */
+ private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED;
+ // Local copy of vr mode enabled state, to avoid calling into VrManager with
+ // the lock held.
+ private boolean mVrModeEnabled = false;
+ private boolean mCanSystemBarsBeShownByUser = true;
+ private int mLockTaskState = LOCK_TASK_MODE_NONE;
+ private boolean mNavBarEmpty;
+
+ private ContentObserver mContentObserver;
+
+ @Inject
+ public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue,
+ SecureSettings secureSettings) {
+ mSysUiContext = context;
+ final Display display = mSysUiContext.getDisplay();
+ mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY
+ ? mSysUiContext : mSysUiContext.createDisplayContext(display);
+ mCommandQueue = commandQueue;
+ mSecureSettings = secureSettings;
+ }
+
+ boolean loadSetting(int currentUserId) {
+ final boolean wasConfirmed = sConfirmed;
+ sConfirmed = false;
+ if (DEBUG) Log.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
+ String value = null;
+ try {
+ value = mSecureSettings.getStringForUser(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
+ UserHandle.USER_CURRENT);
+ sConfirmed = CONFIRMED.equals(value);
+ if (DEBUG) Log.d(TAG, "Loaded sConfirmed=" + sConfirmed);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error loading confirmations, value=" + value, t);
+ }
+ return sConfirmed != wasConfirmed;
+ }
+
+ private static void saveSetting(Context context) {
+ if (DEBUG) Log.d(TAG, "saveSetting()");
+ try {
+ final String value = sConfirmed ? CONFIRMED : null;
+ Settings.Secure.putStringForUser(context.getContentResolver(),
+ Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
+ value,
+ UserHandle.USER_CURRENT);
+ if (DEBUG) Log.d(TAG, "Saved value=" + value);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
+ }
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ if (displayId != mSysUiContext.getDisplayId()) {
+ return;
+ }
+ mHandler.removeMessages(H.SHOW);
+ mHandler.removeMessages(H.HIDE);
+ IVrManager vrManager = IVrManager.Stub.asInterface(
+ ServiceManager.getService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ vrManager.unregisterListener(mVrStateCallbacks);
+ } catch (RemoteException ex) {
+ }
+ }
+ mCommandQueue.removeCallback(this);
+ }
+
+ private void onSettingChanged(int currentUserId) {
+ final boolean changed = loadSetting(currentUserId);
+ // Remove the window if the setting changes to be confirmed.
+ if (changed && sConfirmed) {
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ mHandler.removeMessages(H.SHOW);
+ if (isImmersiveMode) {
+ if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
+ boolean userSetupComplete = (mSecureSettings.getIntForUser(
+ Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0);
+
+ if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed)
+ && userSetupComplete
+ && !mVrModeEnabled
+ && mCanSystemBarsBeShownByUser
+ && !mNavBarEmpty
+ && !UserManager.isDeviceInDemoMode(mDisplayContext)
+ && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+ final Message msg = mHandler.obtainMessage(
+ H.SHOW);
+ msg.arg1 = rootDisplayAreaId;
+ mHandler.sendMessageDelayed(msg, mShowDelayMs);
+ }
+ } else {
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+
+ @Override
+ public void disable(int displayId, int disableFlag, int disableFlag2, boolean animate) {
+ if (mSysUiContext.getDisplayId() != displayId) {
+ return;
+ }
+ final int disableNavigationBar = (DISABLE_HOME | DISABLE_BACK | DISABLE_RECENT);
+ mNavBarEmpty = (disableFlag & disableNavigationBar) == disableNavigationBar;
+ }
+
+ @Override
+ public void confirmImmersivePrompt() {
+ if (mClingWindow != null) {
+ if (DEBUG) Log.d(TAG, "confirmImmersivePrompt()");
+ mHandler.post(mConfirm);
+ }
+ }
+
+ private void handleHide() {
+ if (mClingWindow != null) {
+ if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation");
+ if (mWindowManager != null) {
+ try {
+ mWindowManager.removeView(mClingWindow);
+ } catch (WindowManager.InvalidDisplayException e) {
+ Log.w(TAG, "Fail to hide the immersive confirmation window because of "
+ + e);
+ }
+ mWindowManager = null;
+ mWindowContext = null;
+ }
+ mClingWindow = null;
+ }
+ }
+
+ private WindowManager.LayoutParams getClingWindowLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+ PixelFormat.TRANSLUCENT);
+ lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
+ // Trusted overlay so touches outside the touchable area are allowed to pass through
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+ | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
+ lp.setTitle("ImmersiveModeConfirmation");
+ lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
+ lp.token = getWindowToken();
+ return lp;
+ }
+
+ private FrameLayout.LayoutParams getBubbleLayoutParams() {
+ return new FrameLayout.LayoutParams(
+ mSysUiContext.getResources().getDimensionPixelSize(
+ R.dimen.immersive_mode_cling_width),
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER_HORIZONTAL | Gravity.TOP);
+ }
+
+ /**
+ * @return the window token that's used by all ImmersiveModeConfirmation windows.
+ */
+ IBinder getWindowToken() {
+ return mWindowToken;
+ }
+
+ @Override
+ public void start() {
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ mCommandQueue.addCallback(this);
+
+ final Resources r = mSysUiContext.getResources();
+ mShowDelayMs = r.getInteger(R.integer.dock_enter_exit_duration) * 3L;
+ mCanSystemBarsBeShownByUser = !r.getBoolean(
+ R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
+ R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
+ IVrManager vrManager = IVrManager.Stub.asInterface(
+ ServiceManager.getService(Context.VR_SERVICE));
+ if (vrManager != null) {
+ try {
+ mVrModeEnabled = vrManager.getVrModeState();
+ vrManager.registerListener(mVrStateCallbacks);
+ mVrStateCallbacks.onVrStateChanged(mVrModeEnabled);
+ } catch (RemoteException e) {
+ // Ignore, we cannot do anything if we failed to access vr manager.
+ }
+ }
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onSettingChanged(mSysUiContext.getUserId());
+ }
+ };
+
+ // Register to listen for changes in Settings.Secure settings.
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, mContentObserver,
+ UserHandle.USER_CURRENT);
+ mSecureSettings.registerContentObserverForUser(
+ Settings.Secure.USER_SETUP_COMPLETE, mContentObserver,
+ UserHandle.USER_CURRENT);
+ }
+ }
+
+ private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
+ @Override
+ public void onVrStateChanged(boolean enabled) {
+ mVrModeEnabled = enabled;
+ if (mVrModeEnabled) {
+ mHandler.removeMessages(H.SHOW);
+ mHandler.sendEmptyMessage(H.HIDE);
+ }
+ }
+ };
+
+ private class ClingWindowView extends FrameLayout {
+ private static final int BGCOLOR = 0x80000000;
+ private static final int OFFSET_DP = 96;
+ private static final int ANIMATION_DURATION = 250;
+
+ private final Runnable mConfirm;
+ private final ColorDrawable mColor = new ColorDrawable(0);
+ private final Interpolator mInterpolator;
+ private ValueAnimator mColorAnim;
+ private ViewGroup mClingLayout;
+
+ private Runnable mUpdateLayoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mClingLayout != null && mClingLayout.getParent() != null) {
+ mClingLayout.setLayoutParams(getBubbleLayoutParams());
+ }
+ }
+ };
+
+ private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
+ new ViewTreeObserver.OnComputeInternalInsetsListener() {
+ private final int[] mTmpInt2 = new int[2];
+
+ @Override
+ public void onComputeInternalInsets(
+ ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ // Set touchable region to cover the cling layout.
+ mClingLayout.getLocationInWindow(mTmpInt2);
+ inoutInfo.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ inoutInfo.touchableRegion.set(
+ mTmpInt2[0],
+ mTmpInt2[1],
+ mTmpInt2[0] + mClingLayout.getWidth(),
+ mTmpInt2[1] + mClingLayout.getHeight());
+ }
+ };
+
+ private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
+ post(mUpdateLayoutRunnable);
+ }
+ }
+ };
+
+ ClingWindowView(Context context, Runnable confirm) {
+ super(context);
+ mConfirm = confirm;
+ setBackground(mColor);
+ setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mInterpolator = AnimationUtils
+ .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ mContext.getDisplay().getMetrics(metrics);
+ float density = metrics.density;
+
+ getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
+
+ // create the confirmation cling
+ mClingLayout = (ViewGroup)
+ View.inflate(mSysUiContext, R.layout.immersive_mode_cling, null);
+
+ TypedArray ta = mDisplayContext.obtainStyledAttributes(
+ new int[]{android.R.attr.colorAccent});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ mClingLayout.setBackgroundColor(colorAccent);
+ ImageView expandMore = mClingLayout.findViewById(R.id.immersive_cling_ic_expand_more);
+ if (expandMore != null) {
+ expandMore.setImageTintList(ColorStateList.valueOf(colorAccent));
+ }
+ ImageView lightBgCirc = mClingLayout.findViewById(R.id.immersive_cling_back_bg_light);
+ if (lightBgCirc != null) {
+ // Set transparency to 50%
+ lightBgCirc.setImageAlpha(128);
+ }
+
+ final Button ok = mClingLayout.findViewById(R.id.ok);
+ ok.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mConfirm.run();
+ }
+ });
+ addView(mClingLayout, getBubbleLayoutParams());
+
+ if (ActivityManager.isHighEndGfx()) {
+ final View cling = mClingLayout;
+ cling.setAlpha(0f);
+ cling.setTranslationY(-OFFSET_DP * density);
+
+ postOnAnimation(new Runnable() {
+ @Override
+ public void run() {
+ cling.animate()
+ .alpha(1f)
+ .translationY(0)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(mInterpolator)
+ .withLayer()
+ .start();
+
+ mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
+ mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final int c = (Integer) animation.getAnimatedValue();
+ mColor.setColor(c);
+ }
+ });
+ mColorAnim.setDuration(ANIMATION_DURATION);
+ mColorAnim.setInterpolator(mInterpolator);
+ mColorAnim.start();
+ }
+ });
+ } else {
+ mColor.setColor(BGCOLOR);
+ }
+
+ mContext.registerReceiver(mReceiver,
+ new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent motion) {
+ return true;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ // we will be hiding the nav bar, so layout as if it's already hidden
+ return new WindowInsets.Builder(insets).setInsets(
+ Type.systemBars(), Insets.NONE).build();
+ }
+ }
+
+ /**
+ * To get window manager for the display.
+ *
+ * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the
+ * confirmation window.
+ */
+ @NonNull
+ private WindowManager createWindowManager(int rootDisplayAreaId) {
+ if (mWindowManager != null) {
+ throw new IllegalStateException(
+ "Must not create a new WindowManager while there is an existing one");
+ }
+ // Create window context to specify the RootDisplayArea
+ final Bundle options = getOptionsForWindowContext(rootDisplayAreaId);
+ mWindowContextRootDisplayAreaId = rootDisplayAreaId;
+ mWindowContext = mDisplayContext.createWindowContext(
+ IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
+ mWindowManager = mWindowContext.getSystemService(WindowManager.class);
+ return mWindowManager;
+ }
+
+ /**
+ * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window.
+ * {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}.
+ */
+ @Nullable
+ private Bundle getOptionsForWindowContext(int rootDisplayAreaId) {
+ // In case we don't care which root display area the window manager is specifying.
+ if (rootDisplayAreaId == FEATURE_UNDEFINED) {
+ return null;
+ }
+
+ final Bundle options = new Bundle();
+ options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
+ return options;
+ }
+
+ private void handleShow(int rootDisplayAreaId) {
+ if (mClingWindow != null) {
+ if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) {
+ if (DEBUG) Log.d(TAG, "Immersive mode confirmation has already been shown");
+ return;
+ } else {
+ // Hide the existing confirmation before show a new one in the new root.
+ if (DEBUG) Log.d(TAG, "Immersive mode confirmation was shown in a different root");
+ handleHide();
+ }
+ }
+ if (DEBUG) Log.d(TAG, "Showing immersive mode confirmation");
+ mClingWindow = new ClingWindowView(mDisplayContext, mConfirm);
+ // show the confirmation
+ final WindowManager.LayoutParams lp = getClingWindowLayoutParams();
+ try {
+ createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp);
+ } catch (WindowManager.InvalidDisplayException e) {
+ Log.w(TAG, "Fail to show the immersive confirmation window because of " + e);
+ }
+ }
+
+ private final Runnable mConfirm = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.d(TAG, "mConfirm.run()");
+ if (!sConfirmed) {
+ sConfirmed = true;
+ saveSetting(mDisplayContext);
+ }
+ handleHide();
+ }
+ };
+
+ private final class H extends Handler {
+ private static final int SHOW = 1;
+ private static final int HIDE = 2;
+
+ H(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ return;
+ }
+ switch(msg.what) {
+ case SHOW:
+ handleShow(msg.arg1);
+ break;
+ case HIDE:
+ handleHide();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onLockTaskModeChanged(int lockTaskState) {
+ mLockTaskState = lockTaskState;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 73d844541259..feb02586a820 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1035,7 +1035,13 @@ public class KeyguardIndicationController {
return; // udfps affordance is highlighted, no need to show action to unlock
} else if (mKeyguardUpdateMonitor.isFaceEnrolled()
&& !mKeyguardUpdateMonitor.getIsFaceAuthenticated()) {
- String message = mContext.getString(R.string.keyguard_retry);
+ String message;
+ if (mAccessibilityManager.isEnabled()
+ || mAccessibilityManager.isTouchExplorationEnabled()) {
+ message = mContext.getString(R.string.accesssibility_keyguard_retry);
+ } else {
+ message = mContext.getString(R.string.keyguard_retry);
+ }
mStatusBarKeyguardViewManager.setKeyguardMessage(message, mInitialTextColorState);
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
index 4ec5f46e7771..7a989cfe227a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LegacyNotificationShelfControllerImpl.java
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar;
import android.view.View;
import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController;
import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope;
import com.android.systemui.statusbar.notification.stack.AmbientState;
@@ -52,7 +51,6 @@ public class LegacyNotificationShelfControllerImpl implements NotificationShelfC
mActivatableNotificationViewController = activatableNotificationViewController;
mKeyguardBypassController = keyguardBypassController;
mStatusBarStateController = statusBarStateController;
- mView.setSensitiveRevealAnimEnabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
mOnAttachStateChangeListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index fb88a96c38c2..763400b307fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -27,10 +27,12 @@ import android.app.Notification;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
+import android.hardware.display.DisplayManager;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
@@ -41,6 +43,7 @@ import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.Log;
+import android.view.Display;
import android.view.View;
import android.widget.ImageView;
@@ -74,11 +77,15 @@ import com.android.systemui.util.concurrency.DelayableExecutor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Comparator;
import java.util.HashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import dagger.Lazy;
@@ -138,6 +145,14 @@ public class NotificationMediaManager implements Dumpable {
private BackDropView mBackdrop;
private ImageView mBackdropFront;
private ImageView mBackdropBack;
+ private final Point mTmpDisplaySize = new Point();
+
+ private final DisplayManager mDisplayManager;
+ @Nullable
+ private List<String> mSmallerInternalDisplayUids;
+ private Display mCurrentDisplay;
+
+ private LockscreenWallpaper.WallpaperDrawable mWallapperDrawable;
private final MediaController.Callback mMediaListener = new MediaController.Callback() {
@Override
@@ -184,7 +199,8 @@ public class NotificationMediaManager implements Dumpable {
SysuiColorExtractor colorExtractor,
KeyguardStateController keyguardStateController,
DumpManager dumpManager,
- WallpaperManager wallpaperManager) {
+ WallpaperManager wallpaperManager,
+ DisplayManager displayManager) {
mContext = context;
mMediaArtworkProcessor = mediaArtworkProcessor;
mKeyguardBypassController = keyguardBypassController;
@@ -200,6 +216,7 @@ public class NotificationMediaManager implements Dumpable {
mStatusBarStateController = statusBarStateController;
mColorExtractor = colorExtractor;
mKeyguardStateController = keyguardStateController;
+ mDisplayManager = displayManager;
mIsLockscreenLiveWallpaperEnabled = wallpaperManager.isLockscreenLiveWallpaperEnabled();
setupNotifPipeline();
@@ -477,6 +494,48 @@ public class NotificationMediaManager implements Dumpable {
}
/**
+ * Notify lockscreen wallpaper drawable the current internal display.
+ */
+ public void onDisplayUpdated(Display display) {
+ Trace.beginSection("NotificationMediaManager#onDisplayUpdated");
+ mCurrentDisplay = display;
+ if (mWallapperDrawable != null) {
+ mWallapperDrawable.onDisplayUpdated(isOnSmallerInternalDisplays());
+ }
+ Trace.endSection();
+ }
+
+ private boolean isOnSmallerInternalDisplays() {
+ if (mSmallerInternalDisplayUids == null) {
+ mSmallerInternalDisplayUids = findSmallerInternalDisplayUids();
+ }
+ return mSmallerInternalDisplayUids.contains(mCurrentDisplay.getUniqueId());
+ }
+
+ private List<String> findSmallerInternalDisplayUids() {
+ if (mSmallerInternalDisplayUids != null) {
+ return mSmallerInternalDisplayUids;
+ }
+ List<Display> internalDisplays = Arrays.stream(mDisplayManager.getDisplays(
+ DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED))
+ .filter(display -> display.getType() == Display.TYPE_INTERNAL)
+ .collect(Collectors.toList());
+ if (internalDisplays.isEmpty()) {
+ return List.of();
+ }
+ Display largestDisplay = internalDisplays.stream()
+ .max(Comparator.comparingInt(this::getRealDisplayArea))
+ .orElse(internalDisplays.get(0));
+ internalDisplays.remove(largestDisplay);
+ return internalDisplays.stream().map(Display::getUniqueId).collect(Collectors.toList());
+ }
+
+ private int getRealDisplayArea(Display display) {
+ display.getRealSize(mTmpDisplaySize);
+ return mTmpDisplaySize.x * mTmpDisplaySize.y;
+ }
+
+ /**
* Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper.
*/
public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
@@ -551,7 +610,7 @@ public class NotificationMediaManager implements Dumpable {
mLockscreenWallpaper != null ? mLockscreenWallpaper.getBitmap() : null;
if (lockWallpaper != null) {
artworkDrawable = new LockscreenWallpaper.WallpaperDrawable(
- mBackdropBack.getResources(), lockWallpaper);
+ mBackdropBack.getResources(), lockWallpaper, isOnSmallerInternalDisplays());
// We're in the SHADE mode on the SIM screen - yet we still need to show
// the lockscreen wallpaper in that mode.
allowWhenShade = mStatusBarStateController.getState() == KEYGUARD;
@@ -611,6 +670,10 @@ public class NotificationMediaManager implements Dumpable {
mBackdropBack.setBackgroundColor(0xFFFFFFFF);
mBackdropBack.setImageDrawable(new ColorDrawable(c));
} else {
+ if (artworkDrawable instanceof LockscreenWallpaper.WallpaperDrawable) {
+ mWallapperDrawable =
+ (LockscreenWallpaper.WallpaperDrawable) artworkDrawable;
+ }
mBackdropBack.setImageDrawable(artworkDrawable);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
index 47a4641bcdd9..5ac542b3530f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -112,9 +112,6 @@ public interface NotificationShadeWindowController extends RemoteInputController
/** Sets the state of whether heads up is showing or not. */
default void setHeadsUpShowing(boolean showing) {}
- /** Sets whether the wallpaper supports ambient mode or not. */
- default void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {}
-
/** Gets whether the wallpaper is showing or not. */
default boolean isShowingWallpaper() {
return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 25a1dc6322ba..3f37c60bee8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -24,7 +24,6 @@ import android.content.res.Resources;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.IndentingPrintWriter;
-import android.util.Log;
import android.util.MathUtils;
import android.view.View;
import android.view.ViewGroup;
@@ -40,6 +39,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
import com.android.systemui.animation.ShadeInterpolation;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -95,8 +96,10 @@ public class NotificationShelf extends ActivatableNotificationView implements St
private float mCornerAnimationDistance;
private NotificationShelfController mController;
private float mActualWidth = -1;
- private boolean mSensitiveRevealAnimEnabled;
- private boolean mShelfRefactorFlagEnabled;
+ private final ViewRefactorFlag mSensitiveRevealAnim =
+ new ViewRefactorFlag(Flags.SENSITIVE_REVEAL_ANIM);
+ private final ViewRefactorFlag mShelfRefactor =
+ new ViewRefactorFlag(Flags.NOTIFICATION_SHELF_REFACTOR);
private boolean mCanModifyColorOfNotifications;
private boolean mCanInteract;
private NotificationStackScrollLayout mHostLayout;
@@ -130,7 +133,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
public void bind(AmbientState ambientState,
NotificationStackScrollLayoutController hostLayoutController) {
- assertRefactorFlagDisabled();
+ mShelfRefactor.assertDisabled();
mAmbientState = ambientState;
mHostLayoutController = hostLayoutController;
hostLayoutController.setOnNotificationRemovedListener((child, isTransferInProgress) -> {
@@ -140,7 +143,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
public void bind(AmbientState ambientState, NotificationStackScrollLayout hostLayout,
NotificationRoundnessManager roundnessManager) {
- if (!checkRefactorFlagEnabled()) return;
+ if (!mShelfRefactor.expectEnabled()) return;
mAmbientState = ambientState;
mHostLayout = hostLayout;
mRoundnessManager = roundnessManager;
@@ -268,7 +271,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
- if (mSensitiveRevealAnimEnabled && viewState.hidden) {
+ if (mSensitiveRevealAnim.isEnabled() && viewState.hidden) {
// if the shelf is hidden, position it at the end of the stack (plus the clip
// padding), such that when it appears animated, it will smoothly move in from the
// bottom, without jump cutting any notifications
@@ -279,7 +282,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private int getSpeedBumpIndex() {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mHostLayout.getSpeedBumpIndex();
} else {
return mHostLayoutController.getSpeedBumpIndex();
@@ -413,7 +416,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
expandingAnimated, isLastChild, shelfClipStart);
// TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
- if ((!mSensitiveRevealAnimEnabled && ((isLastChild && !child.isInShelf())
+ if ((!mSensitiveRevealAnim.isEnabled() && ((isLastChild && !child.isInShelf())
|| backgroundForceHidden)) || aboveShelf) {
notificationClipEnd = shelfStart + getIntrinsicHeight();
} else {
@@ -462,7 +465,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
// if the shelf is visible, but if the shelf is hidden, it causes incorrect curling.
// notificationClipEnd handles the discrepancy between a visible and hidden shelf,
// so we use that when on the keyguard (and while animating away) to reduce curling.
- final float keyguardSafeShelfStart = !mSensitiveRevealAnimEnabled
+ final float keyguardSafeShelfStart = !mSensitiveRevealAnim.isEnabled()
&& mAmbientState.isOnKeyguard() ? notificationClipEnd : shelfStart;
updateCornerRoundnessOnScroll(anv, viewStart, keyguardSafeShelfStart);
}
@@ -504,7 +507,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private ExpandableView getHostLayoutChildAt(int index) {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return (ExpandableView) mHostLayout.getChildAt(index);
} else {
return mHostLayoutController.getChildAt(index);
@@ -512,7 +515,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private int getHostLayoutChildCount() {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mHostLayout.getChildCount();
} else {
return mHostLayoutController.getChildCount();
@@ -520,7 +523,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private boolean canModifyColorOfNotifications() {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mCanModifyColorOfNotifications && mAmbientState.isShadeExpanded();
} else {
return mController.canModifyColorOfNotifications();
@@ -583,7 +586,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private boolean isViewAffectedBySwipe(ExpandableView expandableView) {
- if (!mShelfRefactorFlagEnabled) {
+ if (!mShelfRefactor.isEnabled()) {
return mHostLayoutController.isViewAffectedBySwipe(expandableView);
} else {
return mRoundnessManager.isViewAffectedBySwipe(expandableView);
@@ -607,7 +610,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private View getHostLayoutTransientView(int index) {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mHostLayout.getTransientView(index);
} else {
return mHostLayoutController.getTransientView(index);
@@ -615,7 +618,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private int getHostLayoutTransientViewCount() {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mHostLayout.getTransientViewCount();
} else {
return mHostLayoutController.getTransientViewCount();
@@ -961,7 +964,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
@Override
public void onStateChanged(int newState) {
- assertRefactorFlagDisabled();
+ mShelfRefactor.assertDisabled();
mStatusBarState = newState;
updateInteractiveness();
}
@@ -975,7 +978,7 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private boolean canInteract() {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mCanInteract;
} else {
return mStatusBarState == StatusBarState.KEYGUARD;
@@ -1018,32 +1021,18 @@ public class NotificationShelf extends ActivatableNotificationView implements St
return false;
}
- private void assertRefactorFlagDisabled() {
- if (mShelfRefactorFlagEnabled) {
- NotificationShelfController.throwIllegalFlagStateError(false);
- }
- }
-
- private boolean checkRefactorFlagEnabled() {
- if (!mShelfRefactorFlagEnabled) {
- Log.wtf(TAG,
- "Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is disabled.");
- }
- return mShelfRefactorFlagEnabled;
- }
-
public void setController(NotificationShelfController notificationShelfController) {
- assertRefactorFlagDisabled();
+ mShelfRefactor.assertDisabled();
mController = notificationShelfController;
}
public void setCanModifyColorOfNotifications(boolean canModifyColorOfNotifications) {
- if (!checkRefactorFlagEnabled()) return;
+ if (!mShelfRefactor.expectEnabled()) return;
mCanModifyColorOfNotifications = canModifyColorOfNotifications;
}
public void setCanInteract(boolean canInteract) {
- if (!checkRefactorFlagEnabled()) return;
+ if (!mShelfRefactor.expectEnabled()) return;
mCanInteract = canInteract;
updateInteractiveness();
}
@@ -1053,27 +1042,15 @@ public class NotificationShelf extends ActivatableNotificationView implements St
}
private int getIndexOfViewInHostLayout(ExpandableView child) {
- if (mShelfRefactorFlagEnabled) {
+ if (mShelfRefactor.isEnabled()) {
return mHostLayout.indexOfChild(child);
} else {
return mHostLayoutController.indexOfChild(child);
}
}
- /**
- * Set whether the sensitive reveal animation feature flag is enabled
- * @param enabled true if enabled
- */
- public void setSensitiveRevealAnimEnabled(boolean enabled) {
- mSensitiveRevealAnimEnabled = enabled;
- }
-
- public void setRefactorFlagEnabled(boolean enabled) {
- mShelfRefactorFlagEnabled = enabled;
- }
-
public void requestRoundnessResetFor(ExpandableView child) {
- if (!checkRefactorFlagEnabled()) return;
+ if (!mShelfRefactor.expectEnabled()) return;
child.requestRoundnessReset(SHELF_SCROLL);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt
index 1619ddaac85c..8a3e21756c2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.kt
@@ -16,11 +16,8 @@
package com.android.systemui.statusbar
-import android.util.Log
import android.view.View
import android.view.View.OnClickListener
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
@@ -49,29 +46,4 @@ interface NotificationShelfController {
/** @see View.setOnClickListener */
fun setOnClickListener(listener: OnClickListener)
-
- companion object {
- @JvmStatic
- fun assertRefactorFlagDisabled(featureFlags: FeatureFlags) {
- if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
- throwIllegalFlagStateError(expected = false)
- }
- }
-
- @JvmStatic
- fun checkRefactorFlagEnabled(featureFlags: FeatureFlags): Boolean =
- featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR).also { enabled ->
- if (!enabled) {
- Log.wtf("NotifShelf", getErrorMessage(expected = true))
- }
- }
-
- @JvmStatic
- fun throwIllegalFlagStateError(expected: Boolean): Nothing =
- error(getErrorMessage(expected))
-
- private fun getErrorMessage(expected: Boolean): String =
- "Code path not supported when Flags.NOTIFICATION_SHELF_REFACTOR is " +
- if (expected) "disabled" else "enabled"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
deleted file mode 100644
index d6f6c2c281de..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar;
-
-import static com.android.systemui.plugins.DarkIconDispatcher.getTint;
-import static com.android.systemui.plugins.DarkIconDispatcher.isInAreas;
-import static com.android.systemui.statusbar.StatusBarIconView.STATE_DOT;
-import static com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN;
-import static com.android.systemui.statusbar.StatusBarIconView.STATE_ICON;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.settingslib.graph.SignalDrawable;
-import com.android.systemui.DualToneHandler;
-import com.android.systemui.R;
-import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
-import com.android.systemui.statusbar.phone.StatusBarIconController;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
-
-import java.util.ArrayList;
-
-/**
- * View group for the mobile icon in the status bar
- */
-public class StatusBarMobileView extends BaseStatusBarFrameLayout implements DarkReceiver,
- StatusIconDisplayable {
- private static final String TAG = "StatusBarMobileView";
-
- /// Used to show etc dots
- private StatusBarIconView mDotView;
- /// The main icon view
- private LinearLayout mMobileGroup;
- private String mSlot;
- private MobileIconState mState;
- private SignalDrawable mMobileDrawable;
- private View mInoutContainer;
- private ImageView mIn;
- private ImageView mOut;
- private ImageView mMobile, mMobileType, mMobileRoaming;
- private View mMobileRoamingSpace;
- @StatusBarIconView.VisibleState
- private int mVisibleState = STATE_HIDDEN;
- private DualToneHandler mDualToneHandler;
- private boolean mForceHidden;
-
- /**
- * Designated constructor
- *
- * This view is special, in that it is the only view in SystemUI that allows for a configuration
- * override on a MCC/MNC-basis. This means that for every mobile view inflated, we have to
- * construct a context with that override, since the resource system doesn't have a way to
- * handle this for us.
- *
- * @param context A context with resources configured by MCC/MNC
- * @param slot The string key defining which slot this icon refers to. Always "mobile" for the
- * mobile icon
- */
- public static StatusBarMobileView fromContext(
- Context context,
- String slot
- ) {
- LayoutInflater inflater = LayoutInflater.from(context);
- StatusBarMobileView v = (StatusBarMobileView)
- inflater.inflate(R.layout.status_bar_mobile_signal_group, null);
- v.setSlot(slot);
- v.init();
- v.setVisibleState(STATE_ICON);
- return v;
- }
-
- public StatusBarMobileView(Context context) {
- super(context);
- }
-
- public StatusBarMobileView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public StatusBarMobileView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- public void getDrawingRect(Rect outRect) {
- super.getDrawingRect(outRect);
- float translationX = getTranslationX();
- float translationY = getTranslationY();
- outRect.left += translationX;
- outRect.right += translationX;
- outRect.top += translationY;
- outRect.bottom += translationY;
- }
-
- private void init() {
- mDualToneHandler = new DualToneHandler(getContext());
- mMobileGroup = findViewById(R.id.mobile_group);
- mMobile = findViewById(R.id.mobile_signal);
- mMobileType = findViewById(R.id.mobile_type);
- mMobileRoaming = findViewById(R.id.mobile_roaming);
- mMobileRoamingSpace = findViewById(R.id.mobile_roaming_space);
- mIn = findViewById(R.id.mobile_in);
- mOut = findViewById(R.id.mobile_out);
- mInoutContainer = findViewById(R.id.inout_container);
-
- mMobileDrawable = new SignalDrawable(getContext());
- mMobile.setImageDrawable(mMobileDrawable);
-
- initDotView();
- }
-
- private void initDotView() {
- mDotView = new StatusBarIconView(mContext, mSlot, null);
- mDotView.setVisibleState(STATE_DOT);
-
- int width = mContext.getResources().getDimensionPixelSize(R.dimen.status_bar_icon_size_sp);
- LayoutParams lp = new LayoutParams(width, width);
- lp.gravity = Gravity.CENTER_VERTICAL | Gravity.START;
- addView(mDotView, lp);
- }
-
- public void applyMobileState(MobileIconState state) {
- boolean requestLayout = false;
- if (state == null) {
- requestLayout = getVisibility() != View.GONE;
- setVisibility(View.GONE);
- mState = null;
- } else if (mState == null) {
- requestLayout = true;
- mState = state.copy();
- initViewState();
- } else if (!mState.equals(state)) {
- requestLayout = updateState(state.copy());
- }
-
- if (requestLayout) {
- requestLayout();
- }
- }
-
- private void initViewState() {
- setContentDescription(mState.contentDescription);
- if (!mState.visible || mForceHidden) {
- mMobileGroup.setVisibility(View.GONE);
- } else {
- mMobileGroup.setVisibility(View.VISIBLE);
- }
- mMobileDrawable.setLevel(mState.strengthId);
- if (mState.typeId > 0) {
- mMobileType.setContentDescription(mState.typeContentDescription);
- mMobileType.setImageResource(mState.typeId);
- mMobileType.setVisibility(View.VISIBLE);
- } else {
- mMobileType.setVisibility(View.GONE);
- }
- mMobile.setVisibility(mState.showTriangle ? View.VISIBLE : View.GONE);
- mMobileRoaming.setVisibility(mState.roaming ? View.VISIBLE : View.GONE);
- mMobileRoamingSpace.setVisibility(mState.roaming ? View.VISIBLE : View.GONE);
- mIn.setVisibility(mState.activityIn ? View.VISIBLE : View.GONE);
- mOut.setVisibility(mState.activityOut ? View.VISIBLE : View.GONE);
- mInoutContainer.setVisibility((mState.activityIn || mState.activityOut)
- ? View.VISIBLE : View.GONE);
- }
-
- private boolean updateState(MobileIconState state) {
- boolean needsLayout = false;
-
- setContentDescription(state.contentDescription);
- int newVisibility = state.visible && !mForceHidden ? View.VISIBLE : View.GONE;
- if (newVisibility != mMobileGroup.getVisibility() && STATE_ICON == mVisibleState) {
- mMobileGroup.setVisibility(newVisibility);
- needsLayout = true;
- }
- if (mState.strengthId != state.strengthId) {
- mMobileDrawable.setLevel(state.strengthId);
- }
- if (mState.typeId != state.typeId) {
- needsLayout |= state.typeId == 0 || mState.typeId == 0;
- if (state.typeId != 0) {
- mMobileType.setContentDescription(state.typeContentDescription);
- mMobileType.setImageResource(state.typeId);
- mMobileType.setVisibility(View.VISIBLE);
- } else {
- mMobileType.setVisibility(View.GONE);
- }
- }
-
- mMobile.setVisibility(state.showTriangle ? View.VISIBLE : View.GONE);
- mMobileRoaming.setVisibility(state.roaming ? View.VISIBLE : View.GONE);
- mMobileRoamingSpace.setVisibility(state.roaming ? View.VISIBLE : View.GONE);
- mIn.setVisibility(state.activityIn ? View.VISIBLE : View.GONE);
- mOut.setVisibility(state.activityOut ? View.VISIBLE : View.GONE);
- mInoutContainer.setVisibility((state.activityIn || state.activityOut)
- ? View.VISIBLE : View.GONE);
-
- needsLayout |= state.roaming != mState.roaming
- || state.activityIn != mState.activityIn
- || state.activityOut != mState.activityOut
- || state.showTriangle != mState.showTriangle;
-
- mState = state;
- return needsLayout;
- }
-
- @Override
- public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) {
- float intensity = isInAreas(areas, this) ? darkIntensity : 0;
- mMobileDrawable.setTintList(
- ColorStateList.valueOf(mDualToneHandler.getSingleColor(intensity)));
- ColorStateList color = ColorStateList.valueOf(getTint(areas, this, tint));
- mIn.setImageTintList(color);
- mOut.setImageTintList(color);
- mMobileType.setImageTintList(color);
- mMobileRoaming.setImageTintList(color);
- mDotView.setDecorColor(tint);
- mDotView.setIconColor(tint, false);
- }
-
- @Override
- public String getSlot() {
- return mSlot;
- }
-
- public void setSlot(String slot) {
- mSlot = slot;
- }
-
- @Override
- public void setStaticDrawableColor(int color) {
- ColorStateList list = ColorStateList.valueOf(color);
- mMobileDrawable.setTintList(list);
- mIn.setImageTintList(list);
- mOut.setImageTintList(list);
- mMobileType.setImageTintList(list);
- mMobileRoaming.setImageTintList(list);
- mDotView.setDecorColor(color);
- }
-
- @Override
- public void setDecorColor(int color) {
- mDotView.setDecorColor(color);
- }
-
- @Override
- public boolean isIconVisible() {
- return mState.visible && !mForceHidden;
- }
-
- @Override
- public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) {
- if (state == mVisibleState) {
- return;
- }
-
- mVisibleState = state;
- switch (state) {
- case STATE_ICON:
- mMobileGroup.setVisibility(View.VISIBLE);
- mDotView.setVisibility(View.GONE);
- break;
- case STATE_DOT:
- mMobileGroup.setVisibility(View.INVISIBLE);
- mDotView.setVisibility(View.VISIBLE);
- break;
- case STATE_HIDDEN:
- default:
- mMobileGroup.setVisibility(View.INVISIBLE);
- mDotView.setVisibility(View.INVISIBLE);
- break;
- }
- }
-
- /**
- * Forces the state to be hidden (views will be GONE) and if necessary updates the layout.
- *
- * Makes sure that the {@link StatusBarIconController} cannot make it visible while this flag
- * is enabled.
- * @param forceHidden {@code true} if the icon should be GONE in its view regardless of its
- * state.
- * {@code false} if the icon should show as determined by its controller.
- */
- public void forceHidden(boolean forceHidden) {
- if (mForceHidden != forceHidden) {
- mForceHidden = forceHidden;
- updateState(mState);
- requestLayout();
- }
- }
-
- @Override
- @StatusBarIconView.VisibleState
- public int getVisibleState() {
- return mVisibleState;
- }
-
- @VisibleForTesting
- public MobileIconState getState() {
- return mState;
- }
-
- @Override
- public String toString() {
- return "StatusBarMobileView(slot=" + mSlot + " state=" + mState + ")";
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index 324e97294f4e..645595c1f7bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -23,6 +23,7 @@ import android.os.Process;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.view.View;
import androidx.annotation.VisibleForTesting;
@@ -151,4 +152,20 @@ public class VibratorHelper {
BIOMETRIC_ERROR_VIBRATION_EFFECT, reason,
HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
}
+
+ /**
+ * Perform a vibration using a view and the one-way API with flags
+ * @see View#performHapticFeedback(int feedbackConstant, int flags)
+ */
+ public void performHapticFeedback(@NonNull View view, int feedbackConstant, int flags) {
+ view.performHapticFeedback(feedbackConstant, flags);
+ }
+
+ /**
+ * Perform a vibration using a view and the one-way API
+ * @see View#performHapticFeedback(int feedbackConstant)
+ */
+ public void performHapticFeedback(@NonNull View view, int feedbackConstant) {
+ view.performHapticFeedback(feedbackConstant);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index 9aa28c31cfd8..93b9ac61ebec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -1282,7 +1282,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
}
}
String sims = args.getString("sims");
- if (sims != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
+ if (sims != null) {
int num = MathUtils.constrain(Integer.parseInt(sims), 1, 8);
List<SubscriptionInfo> subs = new ArrayList<>();
if (num != mMobileSignalControllers.size()) {
@@ -1305,7 +1305,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
mCallbackHandler.setNoSims(mHasNoSubs, mSimDetected);
}
String mobile = args.getString("mobile");
- if (mobile != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
+ if (mobile != null) {
boolean show = mobile.equals("show");
String datatype = args.getString("datatype");
String slotString = args.getString("slot");
@@ -1390,7 +1390,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
controller.notifyListeners();
}
String carrierNetworkChange = args.getString("carriernetworkchange");
- if (carrierNetworkChange != null && !mStatusBarPipelineFlags.useNewMobileIcons()) {
+ if (carrierNetworkChange != null) {
boolean show = carrierNetworkChange.equals("show");
for (int i = 0; i < mMobileSignalControllers.size(); i++) {
MobileSignalController controller = mMobileSignalControllers.valueAt(i);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index e5ba3ce1fdae..1c7a1860dbb8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.dagger;
import android.app.IActivityManager;
import android.app.WallpaperManager;
import android.content.Context;
+import android.hardware.display.DisplayManager;
import android.os.RemoteException;
import android.service.dreams.IDreamManager;
import android.util.Log;
@@ -146,7 +147,8 @@ public interface CentralSurfacesDependenciesModule {
SysuiColorExtractor colorExtractor,
KeyguardStateController keyguardStateController,
DumpManager dumpManager,
- WallpaperManager wallpaperManager) {
+ WallpaperManager wallpaperManager,
+ DisplayManager displayManager) {
return new NotificationMediaManager(
context,
centralSurfacesOptionalLazy,
@@ -162,7 +164,8 @@ public interface CentralSurfacesDependenciesModule {
colorExtractor,
keyguardStateController,
dumpManager,
- wallpaperManager);
+ wallpaperManager,
+ displayManager);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 56390002490c..6e8b8bdebbe3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -265,6 +265,11 @@ class SystemEventChipAnimationController @Inject constructor(
// not animating then [prepareChipAnimation] will take care of it for us
currentAnimatedView?.let {
updateChipBounds(it, newContentArea)
+ // Since updateCurrentAnimatedView can only be called during an animation, we
+ // have to create a dummy animator here to apply the new chip bounds
+ val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
+ animator.addUpdateListener { updateCurrentAnimatedView() }
+ animator.start()
}
}
})
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 577ad20cb5d4..bac898279c97 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.notification
-import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver
-import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import javax.inject.Inject
@@ -25,12 +23,8 @@ import javax.inject.Inject
class NotifPipelineFlags
@Inject
constructor(
- private val featureFlags: FeatureFlags,
- private val sysPropFlags: FlagResolver,
+ private val featureFlags: FeatureFlags
) {
fun isDevLoggingEnabled(): Boolean =
featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
-
- fun allowDismissOngoing(): Boolean =
- sysPropFlags.isEnabled(NotificationFlags.ALLOW_DISMISS_ONGOING)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 1cf9c1e1f299..1c5aa3cce266 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -3,10 +3,10 @@ package com.android.systemui.statusbar.notification
import android.util.FloatProperty
import android.view.View
import androidx.annotation.FloatRange
-import com.android.systemui.Dependency
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ViewRefactorFlag
import com.android.systemui.statusbar.notification.stack.AnimationProperties
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import kotlin.math.abs
@@ -46,14 +46,14 @@ interface Roundable {
@JvmDefault
val topCornerRadius: Float
get() =
- if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.topCornerRadius
+ if (roundableState.newHeadsUpAnim.isEnabled) roundableState.topCornerRadius
else topRoundness * maxRadius
/** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
@JvmDefault
val bottomCornerRadius: Float
get() =
- if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.bottomCornerRadius
+ if (roundableState.newHeadsUpAnim.isEnabled) roundableState.bottomCornerRadius
else bottomRoundness * maxRadius
/** Get and update the current radii */
@@ -335,13 +335,12 @@ constructor(
internal val targetView: View,
private val roundable: Roundable,
maxRadius: Float,
- private val featureFlags: FeatureFlags = Dependency.get(FeatureFlags::class.java)
+ featureFlags: FeatureFlags? = null
) {
internal var maxRadius = maxRadius
private set
- internal val newHeadsUpAnimFlagEnabled
- get() = featureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS)
+ internal val newHeadsUpAnim = ViewRefactorFlag(featureFlags, Flags.IMPROVED_HUN_ANIMATIONS)
/** Animatable for top roundness */
private val topAnimatable = topAnimatable(roundable)
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 789873675bfa..affd2d186774 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
@@ -791,28 +791,6 @@ public final class NotificationEntry extends ListEntry {
return !mSbn.isOngoing() || !isLocked;
}
- /**
- * @return Can the underlying notification be individually dismissed?
- * @see #canViewBeDismissed()
- */
- // TODO: This logic doesn't belong on NotificationEntry. It should be moved to a controller
- // that can be added as a dependency to any class that needs to answer this question.
- public boolean legacyIsDismissableRecursive() {
- if (mSbn.isOngoing()) {
- return false;
- }
- List<NotificationEntry> children = getAttachedNotifChildren();
- if (children != null && children.size() > 0) {
- for (int i = 0; i < children.size(); i++) {
- NotificationEntry child = children.get(i);
- if (child.getSbn().isOngoing()) {
- return false;
- }
- }
- }
- return true;
- }
-
public boolean canViewBeDismissed() {
if (row == null) return true;
return row.canViewBeDismissed();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 62a0d138fd05..5c2f9a8d28ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -39,7 +39,10 @@ class StackCoordinator @Inject internal constructor(
override fun attach(pipeline: NotifPipeline) {
pipeline.addOnAfterRenderListListener(::onAfterRenderList)
- groupExpansionManagerImpl.attach(pipeline)
+ // TODO(b/282865576): This has an issue where it makes changes to some groups without
+ // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the
+ // group expansion bug.
+ // groupExpansionManagerImpl.attach(pipeline)
}
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 189608072ec7..9ecf50ee4f8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.notification.collection.inflation;
-import static com.android.systemui.flags.Flags.NOTIFICATION_INLINE_REPLY_ANIMATION;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -176,8 +175,6 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
entry.setRow(row);
mNotifBindPipeline.manageRow(entry, row);
mPresenter.onBindRow(row);
- row.setInlineReplyAnimationFlagEnabled(
- mFeatureFlags.isEnabled(NOTIFICATION_INLINE_REPLY_ANIMATION));
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
index b31825207132..78e9a740a547 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
@@ -20,7 +20,6 @@ import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.util.asIndenting
import com.android.systemui.util.withIncreasedIndent
@@ -28,9 +27,7 @@ import java.io.PrintWriter
import javax.inject.Inject
@SysUISingleton
-class NotificationDismissibilityProviderImpl
-@Inject
-constructor(private val notifPipelineFlags: NotifPipelineFlags, dumpManager: DumpManager) :
+class NotificationDismissibilityProviderImpl @Inject constructor(dumpManager: DumpManager) :
NotificationDismissibilityProvider, Dumpable {
init {
@@ -43,11 +40,7 @@ constructor(private val notifPipelineFlags: NotifPipelineFlags, dumpManager: Dum
private set
override fun isDismissable(entry: NotificationEntry): Boolean {
- return if (notifPipelineFlags.allowDismissOngoing()) {
- entry.key !in nonDismissableEntryKeys
- } else {
- entry.legacyIsDismissableRecursive()
- }
+ return entry.key !in nonDismissableEntryKeys
}
@Synchronized
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 4568c0ca1458..46af03a438f5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -21,6 +21,8 @@ import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -44,14 +46,21 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
private final GroupMembershipManager mGroupMembershipManager;
private final Set<OnGroupExpansionChangeListener> mOnGroupChangeListeners = new HashSet<>();
- // Set of summary keys whose groups are expanded
+ /**
+ * Set of summary keys whose groups are expanded.
+ * NOTE: This should not be modified without notifying listeners, so prefer using
+ * {@code setGroupExpanded} when making changes.
+ */
private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
+ private final FeatureFlags mFeatureFlags;
+
@Inject
public GroupExpansionManagerImpl(DumpManager dumpManager,
- GroupMembershipManager groupMembershipManager) {
+ GroupMembershipManager groupMembershipManager, FeatureFlags featureFlags) {
mDumpManager = dumpManager;
mGroupMembershipManager = groupMembershipManager;
+ mFeatureFlags = featureFlags;
}
/**
@@ -85,13 +94,17 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl
@Override
public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
+ boolean changed;
if (expanded) {
- mExpandedGroups.add(groupSummary);
+ changed = mExpandedGroups.add(groupSummary);
} else {
- mExpandedGroups.remove(groupSummary);
+ changed = mExpandedGroups.remove(groupSummary);
}
- sendOnGroupExpandedChange(entry, expanded);
+ // Only notify listeners if something changed.
+ if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE) || changed) {
+ sendOnGroupExpandedChange(entry, expanded);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 908c11a1d076..36a8e9833d39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -566,7 +566,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
@Override
public float getTopCornerRadius() {
- if (isNewHeadsUpAnimFlagEnabled()) {
+ if (mImprovedHunAnimation.isEnabled()) {
return super.getTopCornerRadius();
}
@@ -576,7 +576,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
@Override
public float getBottomCornerRadius() {
- if (isNewHeadsUpAnimFlagEnabled()) {
+ if (mImprovedHunAnimation.isEnabled()) {
return super.getBottomCornerRadius();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index b34c28163abb..ed489a6c5343 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -77,6 +77,7 @@ import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -275,7 +276,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private OnExpandClickListener mOnExpandClickListener;
private View.OnClickListener mOnFeedbackClickListener;
private Path mExpandingClipPath;
- private boolean mIsInlineReplyAnimationFlagEnabled = false;
+ private final ViewRefactorFlag mInlineReplyAnimation =
+ new ViewRefactorFlag(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
// Listener will be called when receiving a long click event.
// Use #setLongPressPosition to optionally assign positional data with the long press.
@@ -3121,10 +3123,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return showingLayout != null && showingLayout.requireRowToHaveOverlappingRendering();
}
- public void setInlineReplyAnimationFlagEnabled(boolean isEnabled) {
- mIsInlineReplyAnimationFlagEnabled = isEnabled;
- }
-
@Override
public void setActualHeight(int height, boolean notifyListeners) {
boolean changed = height != getActualHeight();
@@ -3144,7 +3142,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
int contentHeight = Math.max(getMinHeight(), height);
for (NotificationContentView l : mLayouts) {
- if (mIsInlineReplyAnimationFlagEnabled) {
+ if (mInlineReplyAnimation.isEnabled()) {
l.setContentHeight(height);
} else {
l.setContentHeight(contentHeight);
@@ -3680,6 +3678,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
pw.print(", mShowingPublicInitialized: " + mShowingPublicInitialized);
NotificationContentView showingLayout = getShowingLayout();
pw.print(", privateShowing: " + (showingLayout == mPrivateLayout));
+ pw.print(", mShowNoBackground: " + mShowNoBackground);
pw.println();
showingLayout.dump(pw, args);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a4e8c2ece894..80f5d1939ac0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -21,12 +21,16 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
@@ -71,6 +75,10 @@ import javax.inject.Named;
@NotificationRowScope
public class ExpandableNotificationRowController implements NotifViewController {
private static final String TAG = "NotifRowController";
+
+ static final Uri BUBBLES_SETTING_URI =
+ Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES);
+ private static final String BUBBLES_SETTING_ENABLED_VALUE = "1";
private final ExpandableNotificationRow mView;
private final NotificationListContainer mListContainer;
private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory;
@@ -104,6 +112,23 @@ public class ExpandableNotificationRowController implements NotifViewController
private final ExpandableNotificationRowDragController mDragController;
private final NotificationDismissibilityProvider mDismissibilityProvider;
private final IStatusBarService mStatusBarService;
+
+ private final NotificationSettingsController mSettingsController;
+
+ @VisibleForTesting
+ final NotificationSettingsController.Listener mSettingsListener =
+ new NotificationSettingsController.Listener() {
+ @Override
+ public void onSettingChanged(Uri setting, int userId, String value) {
+ if (BUBBLES_SETTING_URI.equals(setting)) {
+ final int viewUserId = mView.getEntry().getSbn().getUserId();
+ if (viewUserId == UserHandle.USER_ALL || viewUserId == userId) {
+ mView.getPrivateLayout().setBubblesEnabledForUser(
+ BUBBLES_SETTING_ENABLED_VALUE.equals(value));
+ }
+ }
+ }
+ };
private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback =
new ExpandableNotificationRow.ExpandableNotificationRowLogger() {
@Override
@@ -201,6 +226,7 @@ public class ExpandableNotificationRowController implements NotifViewController
FeatureFlags featureFlags,
PeopleNotificationIdentifier peopleNotificationIdentifier,
Optional<BubblesManager> bubblesManagerOptional,
+ NotificationSettingsController settingsController,
ExpandableNotificationRowDragController dragController,
NotificationDismissibilityProvider dismissibilityProvider,
IStatusBarService statusBarService) {
@@ -229,6 +255,7 @@ public class ExpandableNotificationRowController implements NotifViewController
mFeatureFlags = featureFlags;
mPeopleNotificationIdentifier = peopleNotificationIdentifier;
mBubblesManagerOptional = bubblesManagerOptional;
+ mSettingsController = settingsController;
mDragController = dragController;
mMetricsLogger = metricsLogger;
mChildrenContainerLogger = childrenContainerLogger;
@@ -298,12 +325,14 @@ public class ExpandableNotificationRowController implements NotifViewController
NotificationMenuRowPlugin.class, false /* Allow multiple */);
mView.setOnKeyguard(mStatusBarStateController.getState() == KEYGUARD);
mStatusBarStateController.addCallback(mStatusBarStateListener);
+ mSettingsController.addCallback(BUBBLES_SETTING_URI, mSettingsListener);
}
@Override
public void onViewDetachedFromWindow(View v) {
mPluginManager.removePluginListener(mView);
mStatusBarStateController.removeCallback(mStatusBarStateListener);
+ mSettingsController.removeCallback(BUBBLES_SETTING_URI, mSettingsListener);
}
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 7f23c1b89b51..c8f13a6302cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,10 +28,9 @@ import android.util.IndentingPrintWriter;
import android.view.View;
import android.view.ViewOutlineProvider;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
import com.android.systemui.util.DumpUtilsKt;
@@ -50,7 +49,8 @@ public abstract class ExpandableOutlineView extends ExpandableView {
private float mOutlineAlpha = -1f;
private boolean mAlwaysRoundBothCorners;
private Path mTmpPath = new Path();
- private final FeatureFlags mFeatureFlags;
+ protected final ViewRefactorFlag mImprovedHunAnimation =
+ new ViewRefactorFlag(Flags.IMPROVED_HUN_ANIMATIONS);
/**
* {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
@@ -126,7 +126,7 @@ public abstract class ExpandableOutlineView extends ExpandableView {
return EMPTY_PATH;
}
float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
- if (!isNewHeadsUpAnimFlagEnabled() && (topRadius + bottomRadius > height)) {
+ if (!mImprovedHunAnimation.isEnabled() && (topRadius + bottomRadius > height)) {
float overShoot = topRadius + bottomRadius - height;
float currentTopRoundness = getTopRoundness();
float currentBottomRoundness = getBottomRoundness();
@@ -167,7 +167,6 @@ public abstract class ExpandableOutlineView extends ExpandableView {
super(context, attrs);
setOutlineProvider(mProvider);
initDimens();
- mFeatureFlags = Dependency.get(FeatureFlags.class);
}
@Override
@@ -376,8 +375,4 @@ public abstract class ExpandableOutlineView extends ExpandableView {
});
}
- // TODO(b/290365128) replace with ViewRefactorFlag
- protected boolean isNewHeadsUpAnimFlagEnabled() {
- return mFeatureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS);
- }
}
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 20f4429f294b..7b6802f95cda 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
@@ -44,6 +44,7 @@ import android.widget.LinearLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SmartReplyController;
@@ -65,7 +66,6 @@ import com.android.systemui.statusbar.policy.SmartReplyStateInflaterKt;
import com.android.systemui.statusbar.policy.SmartReplyView;
import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
import com.android.systemui.util.Compile;
-import com.android.systemui.wmshell.BubblesManager;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -134,6 +134,7 @@ public class NotificationContentView extends FrameLayout implements Notification
private PeopleNotificationIdentifier mPeopleIdentifier;
private RemoteInputViewSubcomponent.Factory mRemoteInputSubcomponentFactory;
private IStatusBarService mStatusBarService;
+ private boolean mBubblesEnabledForUser;
/**
* List of listeners for when content views become inactive (i.e. not the showing view).
@@ -1440,12 +1441,17 @@ public class NotificationContentView extends FrameLayout implements Notification
}
}
+ @Background
+ public void setBubblesEnabledForUser(boolean enabled) {
+ mBubblesEnabledForUser = enabled;
+ }
+
@VisibleForTesting
boolean shouldShowBubbleButton(NotificationEntry entry) {
boolean isPersonWithShortcut =
mPeopleIdentifier.getPeopleNotificationType(entry)
>= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
- return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+ return mBubblesEnabledForUser
&& isPersonWithShortcut
&& entry.getBubbleMetadata() != null;
}
@@ -2079,6 +2085,7 @@ public class NotificationContentView extends FrameLayout implements Notification
pw.print("null");
}
pw.println();
+ pw.println("mBubblesEnabledForUser: " + mBubblesEnabledForUser);
pw.print("RemoteInputViews { ");
pw.print(" visibleType: " + mVisibleType);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index b2a3780c1024..867e08b2e743 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import dagger.Binds;
import dagger.Module;
@@ -58,9 +59,13 @@ public abstract class NotificationRowModule {
@ElementsIntoSet
@Named(NOTIF_REMOTEVIEWS_FACTORIES)
static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories(
- FeatureFlags featureFlags
+ FeatureFlags featureFlags,
+ PrecomputedTextViewFactory precomputedTextViewFactory
) {
final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>();
+ if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) {
+ replacementFactories.add(precomputedTextViewFactory);
+ }
return replacementFactories;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java
new file mode 100644
index 000000000000..585ff523b9a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.Dumpable;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.settings.SecureSettings;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import javax.inject.Inject;
+
+/**
+ * Centralized controller for listening to Secure Settings changes and informing in-process
+ * listeners, on a background thread.
+ */
+@SysUISingleton
+public class NotificationSettingsController implements Dumpable {
+
+ private final static String TAG = "NotificationSettingsController";
+ private final UserTracker mUserTracker;
+ private final UserTracker.Callback mCurrentUserTrackerCallback;
+ private final Handler mHandler;
+ private final ContentObserver mContentObserver;
+ private final SecureSettings mSecureSettings;
+ private final HashMap<Uri, ArrayList<Listener>> mListeners = new HashMap<>();
+
+ @Inject
+ public NotificationSettingsController(UserTracker userTracker,
+ @Background Handler handler,
+ SecureSettings secureSettings,
+ DumpManager dumpManager) {
+ mUserTracker = userTracker;
+ mHandler = handler;
+ mSecureSettings = secureSettings;
+ mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ synchronized (mListeners) {
+ if (mListeners.containsKey(uri)) {
+ for (Listener listener : mListeners.get(uri)) {
+ notifyListener(listener, uri);
+ }
+ }
+ }
+ }
+ };
+
+ mCurrentUserTrackerCallback = new UserTracker.Callback() {
+ @Override
+ public void onUserChanged(int newUser, Context userContext) {
+ synchronized (mListeners) {
+ if (mListeners.size() > 0) {
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ for (Uri uri : mListeners.keySet()) {
+ mSecureSettings.registerContentObserverForUser(
+ uri, false, mContentObserver, newUser);
+ }
+ }
+ }
+ }
+ };
+ mUserTracker.addCallback(mCurrentUserTrackerCallback, new HandlerExecutor(handler));
+
+ dumpManager.registerNormalDumpable(TAG, this);
+ }
+
+ /**
+ * Register callback whenever the given secure settings changes.
+ *
+ * On registration, will call back on the provided handler with the current value of
+ * the setting.
+ */
+ public void addCallback(@NonNull Uri uri, @NonNull Listener listener) {
+ if (uri == null || listener == null) {
+ return;
+ }
+ synchronized (mListeners) {
+ ArrayList<Listener> currentListeners = mListeners.get(uri);
+ if (currentListeners == null) {
+ currentListeners = new ArrayList<>();
+ }
+ if (!currentListeners.contains(listener)) {
+ currentListeners.add(listener);
+ }
+ mListeners.put(uri, currentListeners);
+ if (currentListeners.size() == 1) {
+ mSecureSettings.registerContentObserverForUser(
+ uri, false, mContentObserver, mUserTracker.getUserId());
+ }
+ }
+ mHandler.post(() -> notifyListener(listener, uri));
+
+ }
+
+ public void removeCallback(Uri uri, Listener listener) {
+ synchronized (mListeners) {
+ ArrayList<Listener> currentListeners = mListeners.get(uri);
+
+ if (currentListeners != null) {
+ currentListeners.remove(listener);
+ }
+ if (currentListeners == null || currentListeners.size() == 0) {
+ mListeners.remove(uri);
+ }
+
+ if (mListeners.size() == 0) {
+ mSecureSettings.unregisterContentObserver(mContentObserver);
+ }
+ }
+ }
+
+ @Override
+ public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+ synchronized (mListeners) {
+ pw.println("Settings Uri Listener List:");
+ for (Uri uri : mListeners.keySet()) {
+ pw.println(" Uri=" + uri);
+ for (Listener listener : mListeners.get(uri)) {
+ pw.println(" Listener=" + listener.getClass().getName());
+ }
+ }
+ }
+ }
+
+ private void notifyListener(Listener listener, Uri uri) {
+ final String setting = uri == null ? null : uri.getLastPathSegment();
+ int userId = mUserTracker.getUserId();
+ listener.onSettingChanged(uri, userId, mSecureSettings.getStringForUser(setting, userId));
+ }
+
+ /**
+ * Listener invoked whenever settings are changed.
+ */
+ public interface Listener {
+ void onSettingChanged(@NonNull Uri setting, int userId, @Nullable String value);
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
new file mode 100644
index 000000000000..b0023305ecdd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.TextView
+import com.android.internal.widget.ImageFloatingTextView
+import javax.inject.Inject
+
+class PrecomputedTextViewFactory @Inject constructor() : NotifRemoteViewsFactory {
+ override fun instantiate(
+ parent: View?,
+ name: String,
+ context: Context,
+ attrs: AttributeSet
+ ): View? {
+ return when (name) {
+ TextView::class.java.name,
+ TextView::class.java.simpleName -> PrecomputedTextView(context, attrs)
+ ImageFloatingTextView::class.java.name ->
+ PrecomputedImageFloatingTextView(context, attrs)
+ else -> null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 23a58d252ba6..22a87a7c9432 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -21,7 +21,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl
@@ -66,7 +65,7 @@ class NotificationShelfViewBinderWrapperControllerImpl @Inject constructor() :
override fun setOnClickListener(listener: View.OnClickListener) = unsupported
private val unsupported: Nothing
- get() = NotificationShelfController.throwIllegalFlagStateError(expected = true)
+ get() = error("Code path not supported when NOTIFICATION_SHELF_REFACTOR is disabled")
}
/** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
@@ -80,8 +79,6 @@ object NotificationShelfViewBinder {
) {
ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
shelf.apply {
- setRefactorFlagEnabled(true)
- setSensitiveRevealAnimEnabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM))
// TODO(278765923): Replace with eventual NotificationIconContainerViewBinder#bind()
notificationIconAreaController.setShelfIcons(shelfIcons)
repeatWhenAttached {
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 b0f3f598cb91..95e74f210c5d 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
@@ -29,7 +29,6 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.StatusBarState;
@@ -57,7 +56,6 @@ public class AmbientState implements Dumpable {
private final SectionProvider mSectionProvider;
private final BypassController mBypassController;
private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
- private final FeatureFlags mFeatureFlags;
/**
* Used to read bouncer states.
*/
@@ -261,13 +259,12 @@ public class AmbientState implements Dumpable {
@NonNull SectionProvider sectionProvider,
@NonNull BypassController bypassController,
@Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager,
- @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator,
- @NonNull FeatureFlags featureFlags) {
+ @NonNull LargeScreenShadeInterpolator largeScreenShadeInterpolator
+ ) {
mSectionProvider = sectionProvider;
mBypassController = bypassController;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
- mFeatureFlags = featureFlags;
reload(context);
dumpManager.registerDumpable(this);
}
@@ -753,10 +750,6 @@ public class AmbientState implements Dumpable {
return mLargeScreenShadeInterpolator;
}
- public FeatureFlags getFeatureFlags() {
- return mFeatureFlags;
- }
-
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("mTopPadding=" + mTopPadding);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index c1ceb3ce5ab6..d71bc2fd6470 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -89,6 +89,7 @@ import com.android.systemui.ExpandHelper;
import com.android.systemui.R;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
import com.android.systemui.shade.ShadeController;
@@ -198,7 +199,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
private Set<Integer> mDebugTextUsedYPositions;
private final boolean mDebugRemoveAnimation;
private final boolean mSensitiveRevealAnimEndabled;
- private boolean mAnimatedInsets;
+ private final ViewRefactorFlag mAnimatedInsets;
+ private final ViewRefactorFlag mShelfRefactor;
private int mContentHeight;
private float mIntrinsicContentHeight;
@@ -621,7 +623,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
mDebugLines = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES);
mDebugRemoveAnimation = featureFlags.isEnabled(Flags.NSSL_DEBUG_REMOVE_ANIMATION);
mSensitiveRevealAnimEndabled = featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
- setAnimatedInsetsEnabled(featureFlags.isEnabled(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS));
+ mAnimatedInsets =
+ new ViewRefactorFlag(featureFlags, Flags.ANIMATED_NOTIFICATION_SHADE_INSETS);
+ mShelfRefactor = new ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR);
mSectionsManager = Dependency.get(NotificationSectionsManager.class);
mScreenOffAnimationController =
Dependency.get(ScreenOffAnimationController.class);
@@ -660,7 +664,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
mGroupMembershipManager = Dependency.get(GroupMembershipManager.class);
mGroupExpansionManager = Dependency.get(GroupExpansionManager.class);
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- if (mAnimatedInsets) {
+ if (mAnimatedInsets.isEnabled()) {
setWindowInsetsAnimationCallback(mInsetsCallback);
}
}
@@ -730,11 +734,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
@VisibleForTesting
- void setAnimatedInsetsEnabled(boolean enabled) {
- mAnimatedInsets = enabled;
- }
-
- @VisibleForTesting
public void updateFooter() {
if (mFooterView == null) {
return;
@@ -1773,7 +1772,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
return;
}
mForcedScroll = v;
- if (mAnimatedInsets) {
+ if (mAnimatedInsets.isEnabled()) {
updateForcedScroll();
} else {
scrollTo(v);
@@ -1822,7 +1821,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
- if (!mAnimatedInsets) {
+ if (!mAnimatedInsets.isEnabled()) {
mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom;
}
mWaterfallTopInset = 0;
@@ -1830,11 +1829,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
if (cutout != null) {
mWaterfallTopInset = cutout.getWaterfallInsets().top;
}
- if (mAnimatedInsets && !mIsInsetAnimationRunning) {
+ if (mAnimatedInsets.isEnabled() && !mIsInsetAnimationRunning) {
// update bottom inset e.g. after rotation
updateBottomInset(insets);
}
- if (!mAnimatedInsets) {
+ if (!mAnimatedInsets.isEnabled()) {
int range = getScrollRange();
if (mOwnScrollY > range) {
// HACK: We're repeatedly getting staggered insets here while the IME is
@@ -2714,7 +2713,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
* @param listener callback for notification removed
*/
public void setOnNotificationRemovedListener(OnNotificationRemovedListener listener) {
- NotificationShelfController.assertRefactorFlagDisabled(mAmbientState.getFeatureFlags());
+ mShelfRefactor.assertDisabled();
mOnNotificationRemovedListener = listener;
}
@@ -2727,7 +2726,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
if (!mChildTransferInProgress) {
onViewRemovedInternal(expandableView, this);
}
- if (mAmbientState.getFeatureFlags().isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+ if (mShelfRefactor.isEnabled()) {
mShelf.requestRoundnessResetFor(expandableView);
} else {
if (mOnNotificationRemovedListener != null) {
@@ -4943,18 +4942,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
@Nullable
public ExpandableView getShelf() {
- if (NotificationShelfController.checkRefactorFlagEnabled(mAmbientState.getFeatureFlags())) {
- return mShelf;
- } else {
- return null;
- }
+ if (!mShelfRefactor.expectEnabled()) return null;
+ return mShelf;
}
public void setShelf(NotificationShelf shelf) {
- if (!NotificationShelfController.checkRefactorFlagEnabled(
- mAmbientState.getFeatureFlags())) {
- return;
- }
+ if (!mShelfRefactor.expectEnabled()) return;
int index = -1;
if (mShelf != null) {
index = indexOfChild(mShelf);
@@ -4968,7 +4961,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
public void setShelfController(NotificationShelfController notificationShelfController) {
- NotificationShelfController.assertRefactorFlagDisabled(mAmbientState.getFeatureFlags());
+ mShelfRefactor.assertDisabled();
int index = -1;
if (mShelf != null) {
index = indexOfChild(mShelf);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index ef7375aa690b..4668aa433533 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -65,6 +65,7 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.shared.model.KeyguardState;
@@ -205,6 +206,7 @@ public class NotificationStackScrollLayoutController {
private boolean mIsInTransitionToAod = false;
private final FeatureFlags mFeatureFlags;
+ private final ViewRefactorFlag mShelfRefactor;
private final NotificationTargetsHelper mNotificationTargetsHelper;
private final SecureSettings mSecureSettings;
private final NotificationDismissibilityProvider mDismissibilityProvider;
@@ -718,6 +720,7 @@ public class NotificationStackScrollLayoutController {
mShadeController = shadeController;
mNotifIconAreaController = notifIconAreaController;
mFeatureFlags = featureFlags;
+ mShelfRefactor = new ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR);
mNotificationTargetsHelper = notificationTargetsHelper;
mSecureSettings = secureSettings;
mDismissibilityProvider = dismissibilityProvider;
@@ -1432,7 +1435,7 @@ public class NotificationStackScrollLayoutController {
}
public void setShelfController(NotificationShelfController notificationShelfController) {
- NotificationShelfController.assertRefactorFlagDisabled(mFeatureFlags);
+ mShelfRefactor.assertDisabled();
mView.setShelfController(notificationShelfController);
}
@@ -1645,12 +1648,12 @@ public class NotificationStackScrollLayoutController {
}
public void setShelf(NotificationShelf shelf) {
- if (!NotificationShelfController.checkRefactorFlagEnabled(mFeatureFlags)) return;
+ if (!mShelfRefactor.expectEnabled()) return;
mView.setShelf(shelf);
}
public int getShelfHeight() {
- if (!NotificationShelfController.checkRefactorFlagEnabled(mFeatureFlags)) {
+ if (!mShelfRefactor.expectEnabled()) {
return 0;
}
ExpandableView shelf = mView.getShelf();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 26b51a95acad..dcd18dd7d1bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -45,6 +45,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -69,6 +70,7 @@ constructor(
private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
private val shadeControllerLazy: Lazy<ShadeController>,
+ private val shadeViewControllerLazy: Lazy<ShadeViewController>,
private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
private val activityLaunchAnimator: ActivityLaunchAnimator,
@@ -896,7 +898,7 @@ constructor(
if (dismissShade) {
return StatusBarLaunchAnimatorController(
animationController,
- it.shadeViewController,
+ shadeViewControllerLazy.get(),
shadeControllerLazy.get(),
notifShadeWindowControllerLazy.get(),
isLaunchForActivity
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 2d8f371aadac..58d6bb024476 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME;
import android.annotation.IntDef;
@@ -30,6 +31,7 @@ import android.metrics.LogMaker;
import android.os.Handler;
import android.os.PowerManager;
import android.os.Trace;
+import android.view.HapticFeedbackConstants;
import androidx.annotation.Nullable;
@@ -51,6 +53,7 @@ import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -177,6 +180,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
private long mLastFpFailureUptimeMillis;
private int mNumConsecutiveFpFailures;
+ private final FeatureFlags mFeatureFlags;
+
private static final class PendingAuthenticated {
public final int userId;
public final BiometricSourceType biometricSourceType;
@@ -280,7 +285,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
LatencyTracker latencyTracker,
ScreenOffAnimationController screenOffAnimationController,
VibratorHelper vibrator,
- SystemClock systemClock
+ SystemClock systemClock,
+ FeatureFlags featureFlags
) {
mPowerManager = powerManager;
mUpdateMonitor = keyguardUpdateMonitor;
@@ -308,6 +314,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
mVibratorHelper = vibrator;
mLogger = biometricUnlockLogger;
mSystemClock = systemClock;
+ mFeatureFlags = featureFlags;
dumpManager.registerDumpable(getClass().getName(), this);
}
@@ -462,7 +469,10 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
Trace.endSection();
};
- if (mMode != MODE_NONE) {
+ final boolean wakingFromDream = mMode == MODE_WAKE_AND_UNLOCK_FROM_DREAM
+ && !mStatusBarStateController.isDozing();
+
+ if (mMode != MODE_NONE && !wakingFromDream) {
wakeUp.run();
}
switch (mMode) {
@@ -747,8 +757,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
mLogger.d("Skip auth success haptic. Power button was recently pressed.");
return;
}
- mVibratorHelper.vibrateAuthSuccess(
- getClass().getSimpleName() + ", type =" + type + "device-entry::success");
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mKeyguardViewController.getViewRootImpl().getView(),
+ HapticFeedbackConstants.CONFIRM
+ );
+ } else {
+ mVibratorHelper.vibrateAuthSuccess(
+ getClass().getSimpleName() + ", type =" + type + "device-entry::success");
+ }
}
private boolean lastWakeupFromPowerButtonWithinHapticThreshold() {
@@ -761,8 +778,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp
}
private void vibrateError(BiometricSourceType type) {
- mVibratorHelper.vibrateAuthError(
- getClass().getSimpleName() + ", type =" + type + "device-entry::error");
+ if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+ mVibratorHelper.performHapticFeedback(
+ mKeyguardViewController.getViewRootImpl().getView(),
+ HapticFeedbackConstants.REJECT
+ );
+ } else {
+ mVibratorHelper.vibrateAuthError(
+ getClass().getSimpleName() + ", type =" + type + "device-entry::error");
+ }
}
private void cleanup() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 2b9c3d33e9b8..5c28be3bc678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -44,7 +44,6 @@ import com.android.systemui.display.data.repository.DisplayMetricsRepository;
import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.qs.QSPanelController;
-import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -195,9 +194,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner {
@Override
Lifecycle getLifecycle();
- /** */
- ShadeViewController getShadeViewController();
-
/** Get the Keyguard Message Area that displays auth messages. */
AuthKeyguardMessageArea getKeyguardMessageArea();
@@ -259,8 +255,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner {
void readyForKeyguardDone();
- void setLockscreenUser(int newUserId);
-
void showKeyguard();
boolean hideKeyguard();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 5fb729cb26f1..6eeb25fdeb9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -51,7 +51,6 @@ import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.app.TaskInfo;
import android.app.UiModeManager;
-import android.app.WallpaperInfo;
import android.app.WallpaperManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
@@ -980,16 +979,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
createAndAddWindows(result);
- if (mWallpaperSupported) {
- // Make sure we always have the most current wallpaper info.
- IntentFilter wallpaperChangedFilter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
- mBroadcastDispatcher.registerReceiver(mWallpaperChangedReceiver, wallpaperChangedFilter,
- null /* handler */, UserHandle.ALL);
- mWallpaperChangedReceiver.onReceive(mContext, null);
- } else if (DEBUG) {
- Log.v(TAG, "start(): no wallpaper service ");
- }
-
// Set up the initial notification state. This needs to happen before CommandQueue.disable()
setUpPresenter();
@@ -1337,7 +1326,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
});
- mScreenOffAnimationController.initialize(this, mLightRevealScrim);
+ mScreenOffAnimationController.initialize(this, mShadeSurface, mLightRevealScrim);
updateLightRevealScrimVisibility();
mShadeSurface.initDependencies(
@@ -1688,8 +1677,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
Trace.endSection();
}
- @Override
- public ShadeViewController getShadeViewController() {
+ protected ShadeViewController getShadeViewController() {
return mShadeSurface;
}
@@ -2072,6 +2060,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
void updateDisplaySize() {
mDisplay.getMetrics(mDisplayMetrics);
mDisplay.getSize(mCurrentDisplaySize);
+ mMediaManager.onDisplayUpdated(mDisplay);
if (DEBUG_GESTURES) {
mGestureRec.tag("display",
String.format("%dx%d", mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
@@ -2164,18 +2153,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
};
/**
- * Notify the shade controller that the current user changed
- *
- * @param newUserId userId of the new user
- */
- @Override
- public void setLockscreenUser(int newUserId) {
- if (mWallpaperSupported) {
- mWallpaperChangedReceiver.onReceive(mContext, null);
- }
- }
-
- /**
* Reload some of our resources when the configuration changes.
*
* We don't reload everything when the configuration changes -- we probably
@@ -3549,33 +3526,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces {
}
};
- /**
- * @deprecated See {@link com.android.systemui.wallpapers.data.repository.WallpaperRepository}
- * instead.
- */
- private final BroadcastReceiver mWallpaperChangedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (!mWallpaperSupported) {
- // Receiver should not have been registered at all...
- Log.wtf(TAG, "WallpaperManager not supported");
- return;
- }
- WallpaperInfo info = mWallpaperManager.getWallpaperInfoForUser(
- mUserTracker.getUserId());
- mWallpaperController.onWallpaperInfoUpdated(info);
-
- final boolean deviceSupportsAodWallpaper = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_dozeSupportsAodWallpaper);
- // If WallpaperInfo is null, it must be ImageWallpaper.
- final boolean supportsAmbientMode = deviceSupportsAodWallpaper
- && (info != null && info.supportsAmbientMode());
-
- mNotificationShadeWindowController.setWallpaperSupportsAmbientMode(supportsAmbientMode);
- mKeyguardViewMediator.setWallpaperSupportsAmbientMode(supportsAmbientMode);
- }
- };
-
private final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
@Override
public void onConfigChanged(Configuration newConfig) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index 8e9f382d8c00..374543dd0c58 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.phone;
import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -34,10 +33,7 @@ import com.android.systemui.demomode.DemoMode;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.statusbar.StatusBarIconView;
-import com.android.systemui.statusbar.StatusBarMobileView;
import com.android.systemui.statusbar.StatusIconDisplayable;
-import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
@@ -52,7 +48,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
private static final String TAG = "DemoStatusIcons";
private final LinearLayout mStatusIcons;
- private final ArrayList<StatusBarMobileView> mMobileViews = new ArrayList<>();
private final ArrayList<ModernStatusBarMobileView> mModernMobileViews = new ArrayList<>();
private final int mIconSize;
@@ -91,7 +86,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
}
public void remove() {
- mMobileViews.clear();
((ViewGroup) getParent()).removeView(this);
}
@@ -127,7 +121,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
mDemoMode = false;
mStatusIcons.setVisibility(View.VISIBLE);
mModernMobileViews.clear();
- mMobileViews.clear();
setVisibility(View.GONE);
}
@@ -236,22 +229,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
}
/**
- * Add a new mobile icon view
- */
- public void addMobileView(MobileIconState state, Context mobileContext) {
- Log.d(TAG, "addMobileView: ");
- StatusBarMobileView view = StatusBarMobileView
- .fromContext(mobileContext, state.slot);
-
- view.applyMobileState(state);
- view.setStaticDrawableColor(mColor);
-
- // mobile always goes at the end
- mMobileViews.add(view);
- addView(view, getChildCount(), createLayoutParams());
- }
-
- /**
* Add a {@link ModernStatusBarMobileView}
* @param mobileContext possibly mcc/mnc overridden mobile context
* @param subId the subscriptionId for this mobile view
@@ -285,8 +262,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
// If we have mobile views, put wifi before them
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
- if (child instanceof StatusBarMobileView
- || child instanceof ModernStatusBarMobileView) {
+ if (child instanceof ModernStatusBarMobileView) {
viewIndex = i;
break;
}
@@ -297,26 +273,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
addView(view, viewIndex, createLayoutParams());
}
- /**
- * Apply an update to a mobile icon view for the given {@link MobileIconState}. For
- * compatibility with {@link MobileContextProvider}, we have to recreate the view every time we
- * update it, since the context (and thus the {@link Configuration}) may have changed
- */
- public void updateMobileState(MobileIconState state, Context mobileContext) {
- Log.d(TAG, "updateMobileState: " + state);
-
- // The mobile config provided by MobileContextProvider could have changed; always recreate
- for (int i = 0; i < mMobileViews.size(); i++) {
- StatusBarMobileView view = mMobileViews.get(i);
- if (view.getState().subId == state.subId) {
- removeView(view);
- }
- }
-
- // Add the replacement or new icon
- addMobileView(state, mobileContext);
- }
-
public void onRemoveIcon(StatusIconDisplayable view) {
if (view.getSlot().equals("wifi")) {
if (view instanceof ModernStatusBarWifiView) {
@@ -324,12 +280,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
removeView(mModernWifiView);
mModernWifiView = null;
}
- } else if (view instanceof StatusBarMobileView) {
- StatusBarMobileView mobileView = matchingMobileView(view);
- if (mobileView != null) {
- removeView(mobileView);
- mMobileViews.remove(mobileView);
- }
} else if (view instanceof ModernStatusBarMobileView) {
ModernStatusBarMobileView mobileView = matchingModernMobileView(
(ModernStatusBarMobileView) view);
@@ -340,21 +290,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
}
}
- private StatusBarMobileView matchingMobileView(StatusIconDisplayable otherView) {
- if (!(otherView instanceof StatusBarMobileView)) {
- return null;
- }
-
- StatusBarMobileView v = (StatusBarMobileView) otherView;
- for (StatusBarMobileView view : mMobileViews) {
- if (view.getState().subId == v.getState().subId) {
- return view;
- }
- }
-
- return null;
- }
-
private ModernStatusBarMobileView matchingModernMobileView(ModernStatusBarMobileView other) {
for (ModernStatusBarMobileView v : mModernMobileViews) {
if (v.getSubId() == other.getSubId()) {
@@ -376,9 +311,6 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
if (mModernWifiView != null) {
mModernWifiView.onDarkChanged(areas, darkIntensity, tint);
}
- for (StatusBarMobileView view : mMobileViews) {
- view.onDarkChanged(areas, darkIntensity, tint);
- }
for (ModernStatusBarMobileView view : mModernMobileViews) {
view.onDarkChanged(areas, darkIntensity, tint);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 7312db6595e5..ed9722e04da0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -311,6 +311,7 @@ public final class DozeServiceHost implements DozeHost {
@Override
public void dozeTimeTick() {
+ mDozeInteractor.dozeTimeTick();
mNotificationPanel.dozeTimeTick();
mAuthController.dozeTimeTick();
if (mAmbientIndicationContainer instanceof DozeReceiver) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
index b2c39f7e289f..92c786fb569f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -306,19 +306,25 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
/**
* Drawable that aligns left horizontally and center vertically (like ImageWallpaper).
+ *
+ * <p>Aligns to the center when showing on the smaller internal display of a multi display
+ * device.
*/
public static class WallpaperDrawable extends DrawableWrapper {
private final ConstantState mState;
private final Rect mTmpRect = new Rect();
+ private boolean mIsOnSmallerInternalDisplays;
- public WallpaperDrawable(Resources r, Bitmap b) {
- this(r, new ConstantState(b));
+ public WallpaperDrawable(Resources r, Bitmap b, boolean isOnSmallerInternalDisplays) {
+ this(r, new ConstantState(b), isOnSmallerInternalDisplays);
}
- private WallpaperDrawable(Resources r, ConstantState state) {
+ private WallpaperDrawable(Resources r, ConstantState state,
+ boolean isOnSmallerInternalDisplays) {
super(new BitmapDrawable(r, state.mBackground));
mState = state;
+ mIsOnSmallerInternalDisplays = isOnSmallerInternalDisplays;
}
@Override
@@ -357,10 +363,17 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
}
dy = (vheight - dheight * scale) * 0.5f;
+ int offsetX = 0;
+ // Offset to show the center area of the wallpaper on a smaller display for multi
+ // display device
+ if (mIsOnSmallerInternalDisplays) {
+ offsetX = bounds.centerX() - (Math.round(dwidth * scale) / 2);
+ }
+
mTmpRect.set(
- bounds.left,
+ bounds.left + offsetX,
bounds.top + Math.round(dy),
- bounds.left + Math.round(dwidth * scale),
+ bounds.left + Math.round(dwidth * scale) + offsetX,
bounds.top + Math.round(dheight * scale + dy));
super.onBoundsChange(mTmpRect);
@@ -371,6 +384,17 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
return mState;
}
+ /**
+ * Update bounds when the hosting display or the display size has changed.
+ *
+ * @param isOnSmallerInternalDisplays true if the drawable is on one of the internal
+ * displays with the smaller area.
+ */
+ public void onDisplayUpdated(boolean isOnSmallerInternalDisplays) {
+ mIsOnSmallerInternalDisplays = isOnSmallerInternalDisplays;
+ onBoundsChange(getBounds());
+ }
+
static class ConstantState extends Drawable.ConstantState {
private final Bitmap mBackground;
@@ -386,7 +410,7 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new WallpaperDrawable(res, this);
+ return new WallpaperDrawable(res, this, /* isOnSmallerInternalDisplays= */ false);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index e18c9d86d74b..0bf0f4b504b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -24,6 +24,8 @@ import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.flags.ViewRefactorFlag;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -88,7 +90,7 @@ public class NotificationIconAreaController implements
private final ArrayList<Rect> mTintAreas = new ArrayList<>();
private final Context mContext;
- private final FeatureFlags mFeatureFlags;
+ private final ViewRefactorFlag mShelfRefactor;
private int mAodIconAppearTranslation;
@@ -120,12 +122,13 @@ public class NotificationIconAreaController implements
Optional<Bubbles> bubblesOptional,
DemoModeController demoModeController,
DarkIconDispatcher darkIconDispatcher,
- FeatureFlags featureFlags, StatusBarWindowController statusBarWindowController,
+ FeatureFlags featureFlags,
+ StatusBarWindowController statusBarWindowController,
ScreenOffAnimationController screenOffAnimationController) {
mContrastColorUtil = ContrastColorUtil.getInstance(context);
mContext = context;
mStatusBarStateController = statusBarStateController;
- mFeatureFlags = featureFlags;
+ mShelfRefactor = new ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR);
mStatusBarStateController.addCallback(this);
mMediaManager = notificationMediaManager;
mDozeParameters = dozeParameters;
@@ -179,12 +182,12 @@ public class NotificationIconAreaController implements
}
public void setupShelf(NotificationShelfController notificationShelfController) {
- NotificationShelfController.assertRefactorFlagDisabled(mFeatureFlags);
+ mShelfRefactor.assertDisabled();
mShelfIcons = notificationShelfController.getShelfIcons();
}
public void setShelfIcons(NotificationIconContainer icons) {
- if (NotificationShelfController.checkRefactorFlagEnabled(mFeatureFlags)) {
+ if (mShelfRefactor.expectEnabled()) {
mShelfIcons = icons;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 3b5aaeac6c21..6dbf70754b3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -546,7 +546,7 @@ public class PhoneStatusBarPolicy
userId = ActivityTaskManager.getService().getLastResumedActivityUserId();
boolean isManagedProfile = mUserManager.isManagedProfile(userId);
String accessibilityString = getManagedProfileAccessibilityString();
- mHandler.post(() -> {
+ mMainExecutor.execute(() -> {
final boolean showIcon;
if (isManagedProfile && (!mKeyguardStateController.isShowing()
|| mKeyguardStateController.isOccluded())) {
@@ -627,6 +627,13 @@ public class PhoneStatusBarPolicy
}
@Override
+ public void appTransitionFinished(int displayId) {
+ if (mDisplayId == displayId) {
+ updateManagedProfile();
+ }
+ }
+
+ @Override
public void onKeyguardShowingChanged() {
updateManagedProfile();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index f0fc1432c5a3..4e136deab5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -28,8 +28,12 @@ import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.ShadeLogger
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -42,6 +46,7 @@ import com.android.systemui.util.view.ViewUtil
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
private const val TAG = "PhoneStatusBarViewController"
@@ -51,10 +56,13 @@ class PhoneStatusBarViewController private constructor(
@Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?,
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
+ private val shadeViewController: ShadeViewController,
+ private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
private val userChipViewModel: StatusBarUserChipViewModel,
private val viewUtil: ViewUtil,
+ private val featureFlags: FeatureFlags,
private val configurationController: ConfigurationController
) : ViewController<PhoneStatusBarView>(view) {
@@ -162,23 +170,33 @@ class PhoneStatusBarViewController private constructor(
return false
}
+ // If scene framework is enabled, route the touch to it and
+ // ignore the rest of the gesture.
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ sceneInteractor.get()
+ .onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
+ // TODO(b/291965119): remove once view is expanded to cover the status bar
+ sceneInteractor.get().setVisible(SceneContainerNames.SYSTEM_UI_DEFAULT, true)
+ return false
+ }
+
if (event.action == MotionEvent.ACTION_DOWN) {
// If the view that would receive the touch is disabled, just have status
// bar eat the gesture.
- if (!centralSurfaces.shadeViewController.isViewEnabled) {
+ if (!shadeViewController.isViewEnabled) {
shadeLogger.logMotionEvent(event,
"onTouchForwardedFromStatusBar: panel view disabled")
return true
}
- if (centralSurfaces.shadeViewController.isFullyCollapsed &&
+ if (shadeViewController.isFullyCollapsed &&
event.y < 1f) {
// b/235889526 Eat events on the top edge of the phone when collapsed
shadeLogger.logMotionEvent(event, "top edge touch ignored")
return true
}
- centralSurfaces.shadeViewController.startTrackingExpansionFromStatusBar()
+ shadeViewController.startTrackingExpansionFromStatusBar()
}
- return centralSurfaces.shadeViewController.handleExternalTouch(event)
+ return shadeViewController.handleExternalTouch(event)
}
}
@@ -222,6 +240,8 @@ class PhoneStatusBarViewController private constructor(
private val userChipViewModel: StatusBarUserChipViewModel,
private val centralSurfaces: CentralSurfaces,
private val shadeController: ShadeController,
+ private val shadeViewController: ShadeViewController,
+ private val sceneInteractor: Provider<SceneInteractor>,
private val shadeLogger: ShadeLogger,
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController,
@@ -241,10 +261,13 @@ class PhoneStatusBarViewController private constructor(
progressProvider.getOrNull(),
centralSurfaces,
shadeController,
+ shadeViewController,
+ sceneInteractor,
shadeLogger,
statusBarMoveFromCenterAnimationController,
userChipViewModel,
viewUtil,
+ featureFlags,
configurationController
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
index c8174669cc65..89c3a02f9401 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone
import android.view.View
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.unfold.FoldAodAnimationController
import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -37,8 +38,12 @@ class ScreenOffAnimationController @Inject constructor(
private val animations: List<ScreenOffAnimation> =
listOfNotNull(foldToAodAnimation, unlockedScreenOffAnimation)
- fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
- animations.forEach { it.initialize(centralSurfaces, lightRevealScrim) }
+ fun initialize(
+ centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
+ lightRevealScrim: LightRevealScrim,
+ ) {
+ animations.forEach { it.initialize(centralSurfaces, shadeViewController, lightRevealScrim) }
wakefulnessLifecycle.addObserver(this)
}
@@ -197,7 +202,11 @@ class ScreenOffAnimationController @Inject constructor(
}
interface ScreenOffAnimation {
- fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {}
+ fun initialize(
+ centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
+ lightRevealScrim: LightRevealScrim,
+ ) {}
/**
* Called when started going to sleep, should return true if the animation will be played
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
index 481cf3ceb197..9a295e63fb9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java
@@ -21,6 +21,7 @@ import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.window.StatusBarWindowController;
@@ -35,6 +36,7 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener
private final NotificationShadeWindowController mNotificationShadeWindowController;
private final StatusBarWindowController mStatusBarWindowController;
private final ShadeViewController mShadeViewController;
+ private final NotificationStackScrollLayoutController mNsslController;
private final KeyguardBypassController mKeyguardBypassController;
private final HeadsUpManagerPhone mHeadsUpManager;
private final StatusBarStateController mStatusBarStateController;
@@ -45,14 +47,15 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener
NotificationShadeWindowController notificationShadeWindowController,
StatusBarWindowController statusBarWindowController,
ShadeViewController shadeViewController,
+ NotificationStackScrollLayoutController nsslController,
KeyguardBypassController keyguardBypassController,
HeadsUpManagerPhone headsUpManager,
StatusBarStateController statusBarStateController,
NotificationRemoteInputManager notificationRemoteInputManager) {
-
mNotificationShadeWindowController = notificationShadeWindowController;
mStatusBarWindowController = statusBarWindowController;
mShadeViewController = shadeViewController;
+ mNsslController = nsslController;
mKeyguardBypassController = keyguardBypassController;
mHeadsUpManager = headsUpManager;
mStatusBarStateController = statusBarStateController;
@@ -85,8 +88,7 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener
//animation
// is finished.
mHeadsUpManager.setHeadsUpGoingAway(true);
- mShadeViewController.getNotificationStackScrollLayoutController()
- .runAfterAnimationFinished(() -> {
+ mNsslController.runAfterAnimationFinished(() -> {
if (!mHeadsUpManager.hasPinnedHeadsUp()) {
mNotificationShadeWindowController.setHeadsUpShowing(false);
mHeadsUpManager.setHeadsUpGoingAway(false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 42b0a4f13e0d..d5cb6b6606c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -15,7 +15,6 @@
package com.android.systemui.statusbar.phone;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
-import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW;
@@ -24,10 +23,8 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.ArraySet;
-import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
@@ -41,12 +38,9 @@ import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.statusbar.BaseStatusBarFrameLayout;
import com.android.systemui.statusbar.StatusBarIconView;
-import com.android.systemui.statusbar.StatusBarMobileView;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
@@ -100,13 +94,10 @@ public interface StatusBarIconController {
*/
void setNewWifiIcon();
- /** */
- void setMobileIcons(String slot, List<MobileIconState> states);
-
/**
- * This method completely replaces {@link #setMobileIcons} with the information from the new
- * mobile data pipeline. Icons will automatically keep their state up to date, so we don't have
- * to worry about funneling MobileIconState objects through anymore.
+ * Notify this class that there is a new set of mobile icons to display, keyed off of this list
+ * of subIds. The icons will be added and bound to the mobile data pipeline via
+ * {@link com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder}.
*/
void setNewMobileIconSubIds(List<Integer> subIds);
/**
@@ -168,14 +159,12 @@ public interface StatusBarIconController {
public DarkIconManager(
LinearLayout linearLayout,
StatusBarLocation location,
- StatusBarPipelineFlags statusBarPipelineFlags,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider mobileContextProvider,
DarkIconDispatcher darkIconDispatcher) {
super(linearLayout,
location,
- statusBarPipelineFlags,
wifiUiAdapter,
mobileUiAdapter,
mobileContextProvider);
@@ -235,7 +224,6 @@ public interface StatusBarIconController {
@SysUISingleton
public static class Factory {
- private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final WifiUiAdapter mWifiUiAdapter;
private final MobileContextProvider mMobileContextProvider;
private final MobileUiAdapter mMobileUiAdapter;
@@ -243,12 +231,10 @@ public interface StatusBarIconController {
@Inject
public Factory(
- StatusBarPipelineFlags statusBarPipelineFlags,
WifiUiAdapter wifiUiAdapter,
MobileContextProvider mobileContextProvider,
MobileUiAdapter mobileUiAdapter,
DarkIconDispatcher darkIconDispatcher) {
- mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiUiAdapter = wifiUiAdapter;
mMobileContextProvider = mobileContextProvider;
mMobileUiAdapter = mobileUiAdapter;
@@ -259,7 +245,6 @@ public interface StatusBarIconController {
return new DarkIconManager(
group,
location,
- mStatusBarPipelineFlags,
mWifiUiAdapter,
mMobileUiAdapter,
mMobileContextProvider,
@@ -277,14 +262,12 @@ public interface StatusBarIconController {
public TintedIconManager(
ViewGroup group,
StatusBarLocation location,
- StatusBarPipelineFlags statusBarPipelineFlags,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider mobileContextProvider
) {
super(group,
location,
- statusBarPipelineFlags,
wifiUiAdapter,
mobileUiAdapter,
mobileContextProvider);
@@ -319,19 +302,16 @@ public interface StatusBarIconController {
@SysUISingleton
public static class Factory {
- private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final WifiUiAdapter mWifiUiAdapter;
private final MobileContextProvider mMobileContextProvider;
private final MobileUiAdapter mMobileUiAdapter;
@Inject
public Factory(
- StatusBarPipelineFlags statusBarPipelineFlags,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider mobileContextProvider
) {
- mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiUiAdapter = wifiUiAdapter;
mMobileUiAdapter = mobileUiAdapter;
mMobileContextProvider = mobileContextProvider;
@@ -341,7 +321,6 @@ public interface StatusBarIconController {
return new TintedIconManager(
group,
location,
- mStatusBarPipelineFlags,
mWifiUiAdapter,
mMobileUiAdapter,
mMobileContextProvider);
@@ -354,7 +333,6 @@ public interface StatusBarIconController {
*/
class IconManager implements DemoModeCommandReceiver {
protected final ViewGroup mGroup;
- private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final MobileContextProvider mMobileContextProvider;
private final LocationBasedWifiViewModel mWifiViewModel;
private final MobileIconsViewModel mMobileIconsViewModel;
@@ -376,27 +354,21 @@ public interface StatusBarIconController {
public IconManager(
ViewGroup group,
StatusBarLocation location,
- StatusBarPipelineFlags statusBarPipelineFlags,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider mobileContextProvider
) {
mGroup = group;
- mStatusBarPipelineFlags = statusBarPipelineFlags;
mMobileContextProvider = mobileContextProvider;
mContext = group.getContext();
mLocation = location;
reloadDimens();
- if (statusBarPipelineFlags.runNewMobileIconsBackend()) {
- // This starts the flow for the new pipeline, and will notify us of changes if
- // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true.
- mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
- MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
- } else {
- mMobileIconsViewModel = null;
- }
+ // This starts the flow for the new pipeline, and will notify us of changes via
+ // {@link #setNewMobileIconIds}
+ mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
+ MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation);
}
@@ -449,9 +421,6 @@ public interface StatusBarIconController {
case TYPE_WIFI_NEW:
return addNewWifiIcon(index, slot);
- case TYPE_MOBILE:
- return addMobileIcon(index, slot, holder.getMobileState());
-
case TYPE_MOBILE_NEW:
return addNewMobileIcon(index, slot, holder.getTag());
}
@@ -479,40 +448,12 @@ public interface StatusBarIconController {
return view;
}
- @VisibleForTesting
- protected StatusIconDisplayable addMobileIcon(
- int index,
- String slot,
- MobileIconState state
- ) {
- if (mStatusBarPipelineFlags.useNewMobileIcons()) {
- throw new IllegalStateException("Attempting to add a mobile icon while the new "
- + "icons are enabled is not supported");
- }
-
- // Use the `subId` field as a key to query for the correct context
- StatusBarMobileView mobileView = onCreateStatusBarMobileView(state.subId, slot);
- mobileView.applyMobileState(state);
- mGroup.addView(mobileView, index, onCreateLayoutParams());
-
- if (mIsInDemoMode) {
- Context mobileContext = mMobileContextProvider
- .getMobileContextForSub(state.subId, mContext);
- mDemoStatusIcons.addMobileView(state, mobileContext);
- }
- return mobileView;
- }
protected StatusIconDisplayable addNewMobileIcon(
int index,
String slot,
int subId
) {
- if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
- throw new IllegalStateException("Attempting to add a mobile icon using the new"
- + "pipeline, but the enabled flag is false.");
- }
-
BaseStatusBarFrameLayout view = onCreateModernStatusBarMobileView(slot, subId);
mGroup.addView(view, index, onCreateLayoutParams());
@@ -536,13 +477,6 @@ public interface StatusBarIconController {
return ModernStatusBarWifiView.constructAndBind(mContext, slot, mWifiViewModel);
}
- private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) {
- Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext);
- StatusBarMobileView view = StatusBarMobileView
- .fromContext(mobileContext, slot);
- return view;
- }
-
private ModernStatusBarMobileView onCreateModernStatusBarMobileView(
String slot, int subId) {
Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext);
@@ -568,15 +502,6 @@ public interface StatusBarIconController {
com.android.internal.R.dimen.status_bar_icon_size_sp);
}
- private void setHeightAndCenter(ImageView imageView, int height) {
- ViewGroup.LayoutParams params = imageView.getLayoutParams();
- params.height = height;
- if (params instanceof LinearLayout.LayoutParams) {
- ((LinearLayout.LayoutParams) params).gravity = Gravity.CENTER_VERTICAL;
- }
- imageView.setLayoutParams(params);
- }
-
protected void onRemoveIcon(int viewIndex) {
if (mIsInDemoMode) {
mDemoStatusIcons.onRemoveIcon((StatusIconDisplayable) mGroup.getChildAt(viewIndex));
@@ -594,9 +519,6 @@ public interface StatusBarIconController {
case TYPE_ICON:
onSetIcon(viewIndex, holder.getIcon());
return;
- case TYPE_MOBILE:
- onSetMobileIcon(viewIndex, holder.getMobileState());
- return;
case TYPE_MOBILE_NEW:
case TYPE_WIFI_NEW:
// Nothing, the new icons update themselves
@@ -606,23 +528,6 @@ public interface StatusBarIconController {
}
}
- public void onSetMobileIcon(int viewIndex, MobileIconState state) {
- View view = mGroup.getChildAt(viewIndex);
- if (view instanceof StatusBarMobileView) {
- ((StatusBarMobileView) view).applyMobileState(state);
- } else {
- // ModernStatusBarMobileView automatically updates via the ViewModel
- throw new IllegalStateException("Cannot update ModernStatusBarMobileView outside of"
- + "the new pipeline");
- }
-
- if (mIsInDemoMode) {
- Context mobileContext = mMobileContextProvider
- .getMobileContextForSub(state.subId, mContext);
- mDemoStatusIcons.updateMobileState(state, mobileContext);
- }
- }
-
@Override
public void dispatchDemoCommand(String command, Bundle args) {
if (!mDemoable) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index d1a02d6cd611..553cbc5f0803 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -39,7 +39,6 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -215,41 +214,10 @@ public class StatusBarIconControllerImpl implements Tunable,
/**
* Accept a list of MobileIconStates, which all live in the same slot(?!), and then are sorted
* by subId. Don't worry this definitely makes sense and works.
- * @param slot da slot
- * @param iconStates All of the mobile icon states
+ * @param subIds list of subscription ID integers that provide the key to the icon to display.
*/
@Override
- public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
- if (mStatusBarPipelineFlags.useNewMobileIcons()) {
- Log.d(TAG, "ignoring old pipeline callbacks, because the new mobile "
- + "icons are enabled");
- return;
- }
- Slot mobileSlot = mStatusBarIconList.getSlot(slot);
-
- // Reverse the sort order to show icons with left to right([Slot1][Slot2]..).
- // StatusBarIconList has UI design that first items go to the right of second items.
- Collections.reverse(iconStates);
-
- for (MobileIconState state : iconStates) {
- StatusBarIconHolder holder = mobileSlot.getHolderForTag(state.subId);
- if (holder == null) {
- holder = StatusBarIconHolder.fromMobileIconState(state);
- setIcon(slot, holder);
- } else {
- holder.setMobileState(state);
- handleSet(slot, holder);
- }
- }
- }
-
- @Override
public void setNewMobileIconSubIds(List<Integer> subIds) {
- if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
- Log.d(TAG, "ignoring new pipeline callback, "
- + "since the new mobile icons are disabled");
- return;
- }
String slotName = mContext.getString(com.android.internal.R.string.status_bar_mobile);
Slot mobileSlot = mStatusBarIconList.getSlot(slotName);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
index 01fd247f54bf..7048a78f31fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
@@ -24,7 +24,6 @@ import android.os.UserHandle;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel;
import java.lang.annotation.Retention;
@@ -35,7 +34,6 @@ import java.lang.annotation.RetentionPolicy;
*/
public class StatusBarIconHolder {
public static final int TYPE_ICON = 0;
- public static final int TYPE_MOBILE = 2;
/**
* TODO (b/249790733): address this once the new pipeline is in place
* This type exists so that the new pipeline (see {@link MobileIconViewModel}) can be used
@@ -63,7 +61,6 @@ public class StatusBarIconHolder {
@IntDef({
TYPE_ICON,
- TYPE_MOBILE,
TYPE_MOBILE_NEW,
TYPE_WIFI_NEW
})
@@ -71,7 +68,6 @@ public class StatusBarIconHolder {
@interface IconType {}
private StatusBarIcon mIcon;
- private MobileIconState mMobileState;
private @IconType int mType = TYPE_ICON;
private int mTag = 0;
@@ -79,7 +75,6 @@ public class StatusBarIconHolder {
public static String getTypeString(@IconType int type) {
switch(type) {
case TYPE_ICON: return "ICON";
- case TYPE_MOBILE: return "MOBILE_OLD";
case TYPE_MOBILE_NEW: return "MOBILE_NEW";
case TYPE_WIFI_NEW: return "WIFI_NEW";
default: return "UNKNOWN";
@@ -103,15 +98,6 @@ public class StatusBarIconHolder {
return holder;
}
- /** */
- public static StatusBarIconHolder fromMobileIconState(MobileIconState state) {
- StatusBarIconHolder holder = new StatusBarIconHolder();
- holder.mMobileState = state;
- holder.mType = TYPE_MOBILE;
- holder.mTag = state.subId;
- return holder;
- }
-
/**
* ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to
* determine icon ordering and building the correct view model
@@ -153,21 +139,10 @@ public class StatusBarIconHolder {
mIcon = icon;
}
- @Nullable
- public MobileIconState getMobileState() {
- return mMobileState;
- }
-
- public void setMobileState(MobileIconState state) {
- mMobileState = state;
- }
-
public boolean isVisible() {
switch (mType) {
case TYPE_ICON:
return mIcon.visible;
- case TYPE_MOBILE:
- return mMobileState.visible;
case TYPE_MOBILE_NEW:
case TYPE_WIFI_NEW:
// The new pipeline controls visibilities via the view model and view binder, so
@@ -188,10 +163,6 @@ public class StatusBarIconHolder {
mIcon.visible = visible;
break;
- case TYPE_MOBILE:
- mMobileState.visible = visible;
- break;
-
case TYPE_MOBILE_NEW:
case TYPE_WIFI_NEW:
// The new pipeline controls visibilities via the view model and view binder, so
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 35285b222f63..ad8530df0523 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -28,7 +28,6 @@ import android.service.vr.IVrStateCallbacks;
import android.util.Log;
import android.util.Slog;
import android.view.View;
-import android.view.accessibility.AccessibilityManager;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.InitController;
@@ -50,7 +49,6 @@ import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
import com.android.systemui.statusbar.notification.domain.interactor.NotificationsInteractor;
@@ -59,7 +57,6 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
-import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
@@ -78,21 +75,17 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
private final NotifShadeEventSource mNotifShadeEventSource;
private final NotificationMediaManager mMediaManager;
private final NotificationGutsManager mGutsManager;
-
private final ShadeViewController mNotificationPanel;
private final HeadsUpManagerPhone mHeadsUpManager;
private final AboveShelfObserver mAboveShelfObserver;
private final DozeScrimController mDozeScrimController;
- private final CentralSurfaces mCentralSurfaces;
private final NotificationsInteractor mNotificationsInteractor;
+ private final NotificationStackScrollLayoutController mNsslController;
private final LockscreenShadeTransitionController mShadeTransitionController;
private final PowerInteractor mPowerInteractor;
private final CommandQueue mCommandQueue;
-
- private final AccessibilityManager mAccessibilityManager;
private final KeyguardManager mKeyguardManager;
private final NotificationShadeWindowController mNotificationShadeWindowController;
- private final NotifPipelineFlags mNotifPipelineFlags;
private final IStatusBarService mBarService;
private final DynamicPrivacyController mDynamicPrivacyController;
private final NotificationListContainer mNotifListContainer;
@@ -113,7 +106,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
NotificationShadeWindowController notificationShadeWindowController,
DynamicPrivacyController dynamicPrivacyController,
KeyguardStateController keyguardStateController,
- CentralSurfaces centralSurfaces,
NotificationsInteractor notificationsInteractor,
LockscreenShadeTransitionController shadeTransitionController,
PowerInteractor powerInteractor,
@@ -123,11 +115,9 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
NotifShadeEventSource notifShadeEventSource,
NotificationMediaManager notificationMediaManager,
NotificationGutsManager notificationGutsManager,
- LockscreenGestureLogger lockscreenGestureLogger,
InitController initController,
NotificationInterruptStateProvider notificationInterruptStateProvider,
NotificationRemoteInputManager remoteInputManager,
- NotifPipelineFlags notifPipelineFlags,
NotificationRemoteInputManager.Callback remoteInputManagerCallback,
NotificationListContainer notificationListContainer) {
mActivityStarter = activityStarter;
@@ -136,9 +126,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
mQsController = quickSettingsController;
mHeadsUpManager = headsUp;
mDynamicPrivacyController = dynamicPrivacyController;
- // TODO: use KeyguardStateController#isOccluded to remove this dependency
- mCentralSurfaces = centralSurfaces;
mNotificationsInteractor = notificationsInteractor;
+ mNsslController = stackScrollerController;
mShadeTransitionController = shadeTransitionController;
mPowerInteractor = powerInteractor;
mCommandQueue = commandQueue;
@@ -149,10 +138,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
mGutsManager = notificationGutsManager;
mAboveShelfObserver = new AboveShelfObserver(stackScrollerController.getView());
mNotificationShadeWindowController = notificationShadeWindowController;
- mNotifPipelineFlags = notifPipelineFlags;
mAboveShelfObserver.setListener(statusBarWindow.findViewById(
R.id.notification_container_parent));
- mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mDozeScrimController = dozeScrimController;
mKeyguardManager = context.getSystemService(KeyguardManager.class);
mBarService = IStatusBarService.Stub.asInterface(
@@ -170,7 +157,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
}
remoteInputManager.setUpWithCallback(
remoteInputManagerCallback,
- mNotificationPanel.getShadeNotificationPresenter().createRemoteInputDelegate());
+ mNsslController.createDelegate());
initController.addPostInitTask(() -> {
mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied);
@@ -202,7 +189,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
}
private void maybeEndAmbientPulse() {
- if (mNotificationPanel.getShadeNotificationPresenter().hasPulsingNotifications()
+ if (mNsslController.getNotificationListContainer().hasPulsingNotifications()
&& !mHeadsUpManager.hasNotifications()) {
// We were showing a pulse for a notification, but no notifications are pulsing anymore.
// Finish the pulse.
@@ -217,7 +204,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
// End old BaseStatusBar.userSwitched
mCommandQueue.animateCollapsePanels();
mMediaManager.clearCurrentMediaNotification();
- mCentralSurfaces.setLockscreenUser(newUserId);
updateMediaMetaData(true, false);
}
@@ -272,22 +258,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
}
};
- private final CheckSaveListener mCheckSaveListener = new CheckSaveListener() {
- @Override
- public void checkSave(Runnable saveImportance, StatusBarNotification sbn) {
- // If the user has security enabled, show challenge if the setting is changed.
- if (mLockscreenUserManager.isLockscreenPublicMode(sbn.getUser().getIdentifier())
- && mKeyguardManager.isKeyguardLocked()) {
- onLockedNotificationImportanceChange(() -> {
- saveImportance.run();
- return true;
- });
- } else {
- saveImportance.run();
- }
- }
- };
-
private final OnSettingsClickListener mOnSettingsClickListener = new OnSettingsClickListener() {
@Override
public void onSettingsClick(String key) {
@@ -309,7 +279,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu
@Override
public boolean suppressAwakeHeadsUp(NotificationEntry entry) {
final StatusBarNotification sbn = entry.getSbn();
- if (mCentralSurfaces.isOccluded()) {
+ if (mKeyguardStateController.isOccluded()) {
boolean devicePublic = mLockscreenUserManager
.isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId());
boolean userPublic = devicePublic
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index 69199966dea6..344e56cf570f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.phone;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Handler;
-import android.telephony.SubscriptionInfo;
import android.util.ArraySet;
import android.util.Log;
@@ -27,7 +26,6 @@ import com.android.settingslib.mobile.TelephonyIcons;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.connectivity.IconState;
-import com.android.systemui.statusbar.connectivity.MobileDataIndicators;
import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.connectivity.SignalCallback;
import com.android.systemui.statusbar.policy.SecurityController;
@@ -71,7 +69,6 @@ public class StatusBarSignalPolicy implements SignalCallback,
// Track as little state as possible, and only for padding purposes
private boolean mIsAirplaneMode = false;
- private ArrayList<MobileIconState> mMobileStates = new ArrayList<>();
private ArrayList<CallIndicatorIconState> mCallIndicatorStates = new ArrayList<>();
private boolean mInitialized;
@@ -102,7 +99,7 @@ public class StatusBarSignalPolicy implements SignalCallback,
mActivityEnabled = mContext.getResources().getBoolean(R.bool.config_showActivity);
}
- /** Call to initilaize and register this classw with the system. */
+ /** Call to initialize and register this class with the system. */
public void init() {
if (mInitialized) {
return;
@@ -189,34 +186,6 @@ public class StatusBarSignalPolicy implements SignalCallback,
CallIndicatorIconState.copyStates(mCallIndicatorStates));
}
- @Override
- public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
- if (DEBUG) {
- Log.d(TAG, "setMobileDataIndicators: " + indicators);
- }
- MobileIconState state = getState(indicators.subId);
- if (state == null) {
- return;
- }
-
- state.visible = indicators.statusIcon.visible && !mHideMobile;
- state.strengthId = indicators.statusIcon.icon;
- state.typeId = indicators.statusType;
- state.contentDescription = indicators.statusIcon.contentDescription;
- state.typeContentDescription = indicators.typeContentDescription;
- state.showTriangle = indicators.showTriangle;
- state.roaming = indicators.roaming;
- state.activityIn = indicators.activityIn && mActivityEnabled;
- state.activityOut = indicators.activityOut && mActivityEnabled;
-
- if (DEBUG) {
- Log.d(TAG, "MobileIconStates: "
- + (mMobileStates == null ? "" : mMobileStates.toString()));
- }
- // Always send a copy to maintain value type semantics
- mIconController.setMobileIcons(mSlotMobile, MobileIconState.copyStates(mMobileStates));
- }
-
private CallIndicatorIconState getNoCallingState(int subId) {
for (CallIndicatorIconState state : mCallIndicatorStates) {
if (state.subId == subId) {
@@ -227,74 +196,8 @@ public class StatusBarSignalPolicy implements SignalCallback,
return null;
}
- private MobileIconState getState(int subId) {
- for (MobileIconState state : mMobileStates) {
- if (state.subId == subId) {
- return state;
- }
- }
- Log.e(TAG, "Unexpected subscription " + subId);
- return null;
- }
-
- /**
- * It is expected that a call to setSubs will be immediately followed by setMobileDataIndicators
- * so we don't have to update the icon manager at this point, just remove the old ones
- * @param subs list of mobile subscriptions, displayed as mobile data indicators (max 8)
- */
- @Override
- public void setSubs(List<SubscriptionInfo> subs) {
- if (DEBUG) Log.d(TAG, "setSubs: " + (subs == null ? "" : subs.toString()));
- if (hasCorrectSubs(subs)) {
- return;
- }
-
- mIconController.removeAllIconsForSlot(mSlotMobile);
- mIconController.removeAllIconsForSlot(mSlotNoCalling);
- mIconController.removeAllIconsForSlot(mSlotCallStrength);
- mMobileStates.clear();
- List<CallIndicatorIconState> noCallingStates = new ArrayList<CallIndicatorIconState>();
- noCallingStates.addAll(mCallIndicatorStates);
- mCallIndicatorStates.clear();
- final int n = subs.size();
- for (int i = 0; i < n; i++) {
- mMobileStates.add(new MobileIconState(subs.get(i).getSubscriptionId()));
- boolean isNewSub = true;
- for (CallIndicatorIconState state : noCallingStates) {
- if (state.subId == subs.get(i).getSubscriptionId()) {
- mCallIndicatorStates.add(state);
- isNewSub = false;
- break;
- }
- }
- if (isNewSub) {
- mCallIndicatorStates.add(
- new CallIndicatorIconState(subs.get(i).getSubscriptionId()));
- }
- }
- }
-
- private boolean hasCorrectSubs(List<SubscriptionInfo> subs) {
- final int N = subs.size();
- if (N != mMobileStates.size()) {
- return false;
- }
- for (int i = 0; i < N; i++) {
- if (mMobileStates.get(i).subId != subs.get(i).getSubscriptionId()) {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public void setNoSims(boolean show, boolean simDetected) {
- // Noop yay!
- }
-
@Override
public void setEthernetIndicators(IconState state) {
- boolean visible = state.visible && !mHideEthernet;
int resId = state.icon;
String description = state.contentDescription;
@@ -324,11 +227,6 @@ public class StatusBarSignalPolicy implements SignalCallback,
}
}
- @Override
- public void setMobileDataEnabled(boolean enabled) {
- // Don't care.
- }
-
/**
* Stores the statusbar state for no Calling & SMS.
*/
@@ -388,117 +286,4 @@ public class StatusBarSignalPolicy implements SignalCallback,
return outStates;
}
}
-
- private static abstract class SignalIconState {
- public boolean visible;
- public boolean activityOut;
- public boolean activityIn;
- public String slot;
- public String contentDescription;
-
- @Override
- public boolean equals(Object o) {
- // Skipping reference equality bc this should be more of a value type
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- SignalIconState that = (SignalIconState) o;
- return visible == that.visible &&
- activityOut == that.activityOut &&
- activityIn == that.activityIn &&
- Objects.equals(contentDescription, that.contentDescription) &&
- Objects.equals(slot, that.slot);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(visible, activityOut, slot);
- }
-
- protected void copyTo(SignalIconState other) {
- other.visible = visible;
- other.activityIn = activityIn;
- other.activityOut = activityOut;
- other.slot = slot;
- other.contentDescription = contentDescription;
- }
- }
-
- /**
- * A little different. This one delegates to SignalDrawable instead of a specific resId
- */
- public static class MobileIconState extends SignalIconState {
- public int subId;
- public int strengthId;
- public int typeId;
- public boolean showTriangle;
- public boolean roaming;
- public boolean needsLeadingPadding;
- public CharSequence typeContentDescription;
-
- private MobileIconState(int subId) {
- super();
- this.subId = subId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- if (!super.equals(o)) {
- return false;
- }
- MobileIconState that = (MobileIconState) o;
- return subId == that.subId
- && strengthId == that.strengthId
- && typeId == that.typeId
- && showTriangle == that.showTriangle
- && roaming == that.roaming
- && needsLeadingPadding == that.needsLeadingPadding
- && Objects.equals(typeContentDescription, that.typeContentDescription);
- }
-
- @Override
- public int hashCode() {
-
- return Objects
- .hash(super.hashCode(), subId, strengthId, typeId, showTriangle, roaming,
- needsLeadingPadding, typeContentDescription);
- }
-
- public MobileIconState copy() {
- MobileIconState copy = new MobileIconState(this.subId);
- copyTo(copy);
- return copy;
- }
-
- public void copyTo(MobileIconState other) {
- super.copyTo(other);
- other.subId = subId;
- other.strengthId = strengthId;
- other.typeId = typeId;
- other.showTriangle = showTriangle;
- other.roaming = roaming;
- other.needsLeadingPadding = needsLeadingPadding;
- other.typeContentDescription = typeContentDescription;
- }
-
- private static List<MobileIconState> copyStates(List<MobileIconState> inStates) {
- ArrayList<MobileIconState> outStates = new ArrayList<>();
- for (MobileIconState state : inStates) {
- MobileIconState copy = new MobileIconState(state.subId);
- state.copyTo(copy);
- outStates.add(copy);
- }
-
- return outStates;
- }
-
- @Override public String toString() {
- return "MobileIconState(subId=" + subId + ", strengthId=" + strengthId
- + ", showTriangle=" + showTriangle + ", roaming=" + roaming
- + ", typeId=" + typeId + ", visible=" + visible + ")";
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 2a039dade059..68a6b3d62bae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -34,15 +34,21 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.ScreenDecorations;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.model.SceneContainerNames;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
import java.io.PrintWriter;
import javax.inject.Inject;
+import javax.inject.Provider;
/**
* Manages what parts of the status bar are touchable. Clients are primarily UI that display in the
@@ -78,6 +84,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
ConfigurationController configurationController,
HeadsUpManagerPhone headsUpManager,
ShadeExpansionStateManager shadeExpansionStateManager,
+ Provider<SceneInteractor> sceneInteractor,
+ Provider<JavaAdapter> javaAdapter,
+ FeatureFlags featureFlags,
UnlockedScreenOffAnimationController unlockedScreenOffAnimationController
) {
mContext = context;
@@ -115,6 +124,12 @@ public final class StatusBarTouchableRegionManager implements Dumpable {
mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged);
+ if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ javaAdapter.get().alwaysCollectFlow(
+ sceneInteractor.get().isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT),
+ this::onShadeExpansionFullyChanged);
+ }
+
mOnComputeInternalInsetsListener = this::onComputeInternalInsets;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt
new file mode 100644
index 000000000000..fbc6b9524a6d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogManagerExt.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Whether dialogs are requesting for affordances to be hidden or not. */
+val SystemUIDialogManager.hideAffordancesRequest: Flow<Boolean>
+ get() = conflatedCallbackFlow {
+ val callback =
+ SystemUIDialogManager.Listener { hideAffordance ->
+ trySendWithFailureLogging(hideAffordance, "dialogHideAffordancesRequest")
+ }
+ registerListener(callback)
+ trySendWithFailureLogging(shouldHideAffordance(), "dialogHideAffordancesRequestInitial")
+ awaitClose { unregisterListener(callback) }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 96a4d900c160..e8da9519c7ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -20,6 +20,7 @@ import com.android.app.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.NotificationShadeWindowController
@@ -66,7 +67,8 @@ class UnlockedScreenOffAnimationController @Inject constructor(
private val powerManager: PowerManager,
private val handler: Handler = Handler()
) : WakefulnessLifecycle.Observer, ScreenOffAnimation {
- private lateinit var mCentralSurfaces: CentralSurfaces
+ private lateinit var centralSurfaces: CentralSurfaces
+ private lateinit var shadeViewController: ShadeViewController
/**
* Whether or not [initialize] has been called to provide us with the StatusBar,
* NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen
@@ -126,7 +128,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
lightRevealAnimator.start()
}
- val animatorDurationScaleObserver = object : ContentObserver(null) {
+ private val animatorDurationScaleObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
updateAnimatorDurationScale()
}
@@ -134,11 +136,13 @@ class UnlockedScreenOffAnimationController @Inject constructor(
override fun initialize(
centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
lightRevealScrim: LightRevealScrim
) {
this.initialized = true
this.lightRevealScrim = lightRevealScrim
- this.mCentralSurfaces = centralSurfaces
+ this.centralSurfaces = centralSurfaces
+ this.shadeViewController = shadeViewController
updateAnimatorDurationScale()
globalSettings.registerContentObserver(
@@ -184,7 +188,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Cancel any existing CUJs before starting the animation
interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
-
+ PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
PropertyAnimator.setProperty(
keyguardView, AnimatableProperty.ALPHA, 1f,
AnimationProperties()
@@ -198,7 +202,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Tell the CentralSurfaces to become keyguard for real - we waited on that
// since it is slow and would have caused the animation to jank.
- mCentralSurfaces.updateIsKeyguard()
+ centralSurfaces.updateIsKeyguard()
// Run the callback given to us by the KeyguardVisibilityHelper.
after.run()
@@ -251,7 +255,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
// changed parts of the UI (such as showing AOD in the shade) without actually changing
// the StatusBarState. This ensures that the UI definitely reflects the desired state.
- mCentralSurfaces.updateIsKeyguard(true /* forceStateChange */)
+ centralSurfaces.updateIsKeyguard(true /* forceStateChange */)
}
}
@@ -280,7 +284,7 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// Show AOD. That'll cause the KeyguardVisibilityHelper to call
// #animateInKeyguard.
- mCentralSurfaces.shadeViewController.showAodUi()
+ shadeViewController.showAodUi()
}
}, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
@@ -328,8 +332,8 @@ class UnlockedScreenOffAnimationController @Inject constructor(
// We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
// already expanded and showing notifications/QS, the animation looks really messy. For now,
// disable it if the notification panel is expanded.
- if ((!this::mCentralSurfaces.isInitialized ||
- mCentralSurfaces.shadeViewController.isPanelExpanded) &&
+ if ((!this::centralSurfaces.isInitialized ||
+ shadeViewController.isPanelExpanded) &&
// Status bar might be expanded because we have started
// playing the animation already
!isAnimationPlaying()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 29829e46cda7..6e51ed0eba37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -18,8 +18,6 @@ package com.android.systemui.statusbar.pipeline
import android.content.Context
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
import javax.inject.Inject
/** All flagging methods related to the new status bar pipeline (see b/238425913). */
@@ -28,30 +26,10 @@ class StatusBarPipelineFlags
@Inject
constructor(
context: Context,
- private val featureFlags: FeatureFlags,
) {
private val mobileSlot = context.getString(com.android.internal.R.string.status_bar_mobile)
private val wifiSlot = context.getString(com.android.internal.R.string.status_bar_wifi)
- /** True if we should display the mobile icons using the new status bar data pipeline. */
- fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS)
-
- /**
- * True if we should run the new mobile icons backend to get the logging.
- *
- * Does *not* affect whether we render the mobile icons using the new backend data. See
- * [useNewMobileIcons] for that.
- */
- fun runNewMobileIconsBackend(): Boolean =
- featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS_BACKEND) || useNewMobileIcons()
-
- /**
- * Returns true if we should apply some coloring to the icons that were rendered with the new
- * pipeline to help with debugging.
- */
- fun useDebugColoring(): Boolean =
- featureFlags.isEnabled(Flags.NEW_STATUS_BAR_ICONS_DEBUG_COLORING)
-
/**
* For convenience in the StatusBarIconController, we want to gate some actions based on slot
* name and the flag together.
@@ -59,5 +37,5 @@ constructor(
* @return true if this icon is controlled by any of the status bar pipeline flags
*/
fun isIconControlledByFlags(slotName: String): Boolean =
- slotName == wifiSlot || (slotName == mobileSlot && useNewMobileIcons())
+ slotName == wifiSlot || slotName == mobileSlot
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 27cc64f9a8e8..0e99c67a0d80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -19,12 +19,10 @@ package com.android.systemui.statusbar.pipeline.dagger
import android.net.wifi.WifiManager
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel
-import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModelImpl
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -46,6 +44,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinderImpl
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel
+import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModelImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
@@ -58,9 +58,9 @@ import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
-import kotlinx.coroutines.flow.Flow
import java.util.function.Supplier
import javax.inject.Named
+import kotlinx.coroutines.flow.Flow
@Module
abstract class StatusBarPipelineModule {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index a05ab849088d..d7fcf4876c28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -20,7 +20,6 @@ import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.StatusBarIconController
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import java.io.PrintWriter
@@ -46,24 +45,18 @@ constructor(
val mobileIconsViewModel: MobileIconsViewModel,
private val logger: MobileViewLogger,
@Application private val scope: CoroutineScope,
- private val statusBarPipelineFlags: StatusBarPipelineFlags,
) : CoreStartable {
private var isCollecting: Boolean = false
private var lastValue: List<Int>? = null
override fun start() {
- // Only notify the icon controller if we want to *render* the new icons.
- // Note that this flow may still run if
- // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
- // get the logging data without rendering.
- if (statusBarPipelineFlags.useNewMobileIcons()) {
- scope.launch {
- isCollecting = true
- mobileIconsViewModel.subscriptionIdsFlow.collectLatest {
- logger.logUiAdapterSubIdsSentToIconController(it)
- lastValue = it
- iconController.setNewMobileIconSubIds(it)
- }
+ // Start notifying the icon controller of subscriptions
+ scope.launch {
+ isCollecting = true
+ mobileIconsViewModel.subscriptionIdsFlow.collectLatest {
+ logger.logUiAdapterSubIdsSentToIconController(it)
+ lastValue = it
+ iconController.setNewMobileIconSubIds(it)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index a2a247a279fb..c22110959fc1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -183,16 +183,10 @@ object MobileIconBinder {
}
override fun onIconTintChanged(newTint: Int) {
- if (viewModel.useDebugColoring) {
- return
- }
iconTint.value = newTint
}
override fun onDecorTintChanged(newTint: Int) {
- if (viewModel.useDebugColoring) {
- return
- }
decorTint.value = newTint
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
index f775940140cc..a51982c41255 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt
@@ -18,7 +18,6 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import android.graphics.Color
import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
/**
@@ -32,24 +31,14 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
*/
abstract class LocationBasedMobileViewModel(
val commonImpl: MobileIconViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
- debugTint: Int,
val locationName: String,
val verboseLogger: VerboseMobileViewLogger?,
) : MobileIconViewModelCommon by commonImpl {
- val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring()
-
- val defaultColor: Int =
- if (useDebugColoring) {
- debugTint
- } else {
- Color.WHITE
- }
+ val defaultColor: Int = Color.WHITE
companion object {
fun viewModelForLocation(
commonImpl: MobileIconViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
verboseMobileViewLogger: VerboseMobileViewLogger,
loc: StatusBarLocation,
): LocationBasedMobileViewModel =
@@ -57,39 +46,31 @@ abstract class LocationBasedMobileViewModel(
StatusBarLocation.HOME ->
HomeMobileIconViewModel(
commonImpl,
- statusBarPipelineFlags,
verboseMobileViewLogger,
)
- StatusBarLocation.KEYGUARD ->
- KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags)
- StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, statusBarPipelineFlags)
+ StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl)
+ StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl)
}
}
}
class HomeMobileIconViewModel(
commonImpl: MobileIconViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
verboseMobileViewLogger: VerboseMobileViewLogger,
) :
MobileIconViewModelCommon,
LocationBasedMobileViewModel(
commonImpl,
- statusBarPipelineFlags,
- debugTint = Color.CYAN,
locationName = "Home",
verboseMobileViewLogger,
)
class QsMobileIconViewModel(
commonImpl: MobileIconViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
) :
MobileIconViewModelCommon,
LocationBasedMobileViewModel(
commonImpl,
- statusBarPipelineFlags,
- debugTint = Color.GREEN,
locationName = "QS",
// Only do verbose logging for the Home location.
verboseLogger = null,
@@ -97,13 +78,10 @@ class QsMobileIconViewModel(
class KeyguardMobileIconViewModel(
commonImpl: MobileIconViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
) :
MobileIconViewModelCommon,
LocationBasedMobileViewModel(
commonImpl,
- statusBarPipelineFlags,
- debugTint = Color.MAGENTA,
locationName = "Keyguard",
// Only do verbose logging for the Home location.
verboseLogger = null,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 40b8c90fb9f5..5cf887e4d41a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -98,7 +98,6 @@ constructor(
val common = commonViewModelForSub(subId)
return LocationBasedMobileViewModel.viewModelForLocation(
common,
- statusBarPipelineFlags,
verboseLogger,
location,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
index 6d7182376fab..7a60d96d1661 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
@@ -23,7 +23,6 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel.Companion.viewModelForLocation
@@ -46,7 +45,6 @@ class WifiUiAdapter
constructor(
private val iconController: StatusBarIconController,
private val wifiViewModel: WifiViewModel,
- private val statusBarPipelineFlags: StatusBarPipelineFlags,
) {
/**
* Binds the container for all the status bar icons to a view model, so that we inflate the wifi
@@ -60,8 +58,7 @@ constructor(
statusBarIconGroup: ViewGroup,
location: StatusBarLocation,
): LocationBasedWifiViewModel {
- val locationViewModel =
- viewModelForLocation(wifiViewModel, statusBarPipelineFlags, location)
+ val locationViewModel = viewModelForLocation(wifiViewModel, location)
statusBarIconGroup.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index e819c4fc96ac..3082a6629dc2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -157,16 +157,10 @@ object WifiViewBinder {
}
override fun onIconTintChanged(newTint: Int) {
- if (viewModel.useDebugColoring) {
- return
- }
iconTint.value = newTint
}
override fun onDecorTintChanged(newTint: Int) {
- if (viewModel.useDebugColoring) {
- return
- }
decorTint.value = newTint
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index b731a41d442b..cd5b92cf24c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -18,7 +18,6 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.graphics.Color
import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
/**
* A view model for a wifi icon in a specific location. This allows us to control parameters that
@@ -27,18 +26,9 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
* Must be subclassed for each distinct location.
*/
abstract class LocationBasedWifiViewModel(
- val commonImpl: WifiViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
- debugTint: Int,
+ private val commonImpl: WifiViewModelCommon,
) : WifiViewModelCommon by commonImpl {
- val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring()
-
- val defaultColor: Int =
- if (useDebugColoring) {
- debugTint
- } else {
- Color.WHITE
- }
+ val defaultColor: Int = Color.WHITE
companion object {
/**
@@ -47,13 +37,12 @@ abstract class LocationBasedWifiViewModel(
*/
fun viewModelForLocation(
commonImpl: WifiViewModelCommon,
- flags: StatusBarPipelineFlags,
location: StatusBarLocation,
): LocationBasedWifiViewModel =
when (location) {
- StatusBarLocation.HOME -> HomeWifiViewModel(commonImpl, flags)
- StatusBarLocation.KEYGUARD -> KeyguardWifiViewModel(commonImpl, flags)
- StatusBarLocation.QS -> QsWifiViewModel(commonImpl, flags)
+ StatusBarLocation.HOME -> HomeWifiViewModel(commonImpl)
+ StatusBarLocation.KEYGUARD -> KeyguardWifiViewModel(commonImpl)
+ StatusBarLocation.QS -> QsWifiViewModel(commonImpl)
}
}
}
@@ -64,23 +53,14 @@ abstract class LocationBasedWifiViewModel(
*/
class HomeWifiViewModel(
commonImpl: WifiViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
-) :
- WifiViewModelCommon,
- LocationBasedWifiViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.CYAN)
+) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl)
/** A view model for the wifi icon shown on keyguard (lockscreen). */
class KeyguardWifiViewModel(
commonImpl: WifiViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
-) :
- WifiViewModelCommon,
- LocationBasedWifiViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.MAGENTA)
+) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl)
/** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */
class QsWifiViewModel(
commonImpl: WifiViewModelCommon,
- statusBarPipelineFlags: StatusBarPipelineFlags,
-) :
- WifiViewModelCommon,
- LocationBasedWifiViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.GREEN)
+) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 02b7e9176cf2..e00365d8fbcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -30,6 +30,7 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
+import android.os.Trace;
import android.os.UserHandle;
import android.text.Editable;
import android.text.SpannedString;
@@ -1032,10 +1033,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
}
private void hideIme() {
+ Trace.beginSection("RemoteEditText#hideIme");
final WindowInsetsController insetsController = getWindowInsetsController();
if (insetsController != null) {
insetsController.hide(WindowInsets.Type.ime());
}
+ Trace.endSection();
}
private void defocusIfNeeded(boolean animate) {
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index 7ed56e72f759..7aeba666c95a 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -88,7 +88,7 @@ constructor(
private val chipbarAnimator: ChipbarAnimator,
private val falsingManager: FalsingManager,
private val falsingCollector: FalsingCollector,
- private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler?,
+ private val swipeChipbarAwayGestureHandler: SwipeChipbarAwayGestureHandler,
private val viewUtil: ViewUtil,
private val vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder,
@@ -289,10 +289,6 @@ constructor(
}
private fun updateGestureListening() {
- if (swipeChipbarAwayGestureHandler == null) {
- return
- }
-
val currentDisplayInfo = getCurrentDisplayInfo()
if (currentDisplayInfo != null && currentDisplayInfo.info.allowSwipeToDismiss) {
swipeChipbarAwayGestureHandler.setViewFetcher { currentDisplayInfo.view }
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
index 9dbc4b398ab3..80de52332c23 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/SwipeChipbarAwayGestureHandler.kt
@@ -19,10 +19,12 @@ package com.android.systemui.temporarydisplay.chipbar
import android.content.Context
import android.view.MotionEvent
import android.view.View
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.settings.DisplayTracker
import com.android.systemui.statusbar.gesture.SwipeUpGestureHandler
import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
import com.android.systemui.util.boundsOnScreen
+import javax.inject.Inject
/**
* A class to detect when a user has swiped the chipbar away.
@@ -30,7 +32,10 @@ import com.android.systemui.util.boundsOnScreen
* Effectively [SysUISingleton]. But, this shouldn't be created if the gesture isn't enabled. See
* [TemporaryDisplayModule.provideSwipeChipbarAwayGestureHandler].
*/
-class SwipeChipbarAwayGestureHandler(
+@SysUISingleton
+class SwipeChipbarAwayGestureHandler
+@Inject
+constructor(
context: Context,
displayTracker: DisplayTracker,
logger: SwipeUpGestureLogger,
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
index cae13086f592..2d055736dcc3 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/dagger/TemporaryDisplayModule.kt
@@ -16,14 +16,9 @@
package com.android.systemui.temporarydisplay.dagger
-import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
-import com.android.systemui.media.taptotransfer.MediaTttFlags
-import com.android.systemui.settings.DisplayTracker
-import com.android.systemui.statusbar.gesture.SwipeUpGestureLogger
-import com.android.systemui.temporarydisplay.chipbar.SwipeChipbarAwayGestureHandler
import dagger.Module
import dagger.Provides
@@ -36,20 +31,5 @@ interface TemporaryDisplayModule {
fun provideChipbarLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("ChipbarLog", 40)
}
-
- @Provides
- @SysUISingleton
- fun provideSwipeChipbarAwayGestureHandler(
- mediaTttFlags: MediaTttFlags,
- context: Context,
- displayTracker: DisplayTracker,
- logger: SwipeUpGestureLogger,
- ): SwipeChipbarAwayGestureHandler? {
- return if (mediaTttFlags.isMediaTttDismissGestureEnabled()) {
- SwipeChipbarAwayGestureHandler(context, displayTracker, logger)
- } else {
- null
- }
- }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
index 5d09e064604a..a501e87902a8 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
@@ -20,6 +20,11 @@ import android.content.Intent;
import com.android.systemui.Dependency;
+/**
+ * @deprecated Don't use this class to listen to Secure Settings. Use {@code SecureSettings} instead
+ * or {@code SettingsObserver} to be able to specify the handler.
+ */
+@Deprecated
public abstract class TunerService {
public static final String ACTION_CLEAR = "com.android.systemui.action.CLEAR_TUNER";
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 8cfe2eac3d33..ccc0a79d2cfe 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -56,7 +56,11 @@ import javax.inject.Inject;
/**
+ * @deprecated Don't use this class to listen to Secure Settings. Use {@code SecureSettings} instead
+ * or {@code SettingsObserver} to be able to specify the handler.
+ * This class will interact with SecureSettings using the main looper.
*/
+@Deprecated
@SysUISingleton
public class TunerServiceImpl extends TunerService {
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 38226ec45910..95e1e430a51e 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -44,8 +44,7 @@ import com.android.systemui.recents.RecentsImplementation;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
-import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeControllerEmptyImpl;
+import com.android.systemui.shade.ShadeEmptyImplModule;
import com.android.systemui.shade.ShadeExpansionStateManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationListener;
@@ -94,6 +93,7 @@ import javax.inject.Named;
PowerModule.class,
QSModule.class,
ReferenceScreenshotModule.class,
+ ShadeEmptyImplModule.class,
StatusBarEventsModule.class,
VolumeModule.class,
}
@@ -137,9 +137,6 @@ public abstract class TvSystemUIModule {
@Binds
abstract DockManager bindDockManager(DockManagerImpl dockManager);
- @Binds
- abstract ShadeController provideShadeController(ShadeControllerEmptyImpl shadeController);
-
@SysUISingleton
@Provides
@Named(ALLOW_NOTIFICATION_LONG_PRESS_NAME)
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index eed7950abacb..098d51e94fc7 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
@@ -31,6 +31,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.shade.ShadeFoldAnimator
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.ScreenOffAnimation
@@ -62,7 +63,7 @@ constructor(
private val keyguardInteractor: Lazy<KeyguardInteractor>,
) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer {
- private lateinit var centralSurfaces: CentralSurfaces
+ private lateinit var shadeViewController: ShadeViewController
private var isFolded = false
private var isFoldHandled = true
@@ -87,14 +88,18 @@ constructor(
)
}
- override fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {
- this.centralSurfaces = centralSurfaces
+ override fun initialize(
+ centralSurfaces: CentralSurfaces,
+ shadeViewController: ShadeViewController,
+ lightRevealScrim: LightRevealScrim,
+ ) {
+ this.shadeViewController = shadeViewController
deviceStateManager.registerCallback(mainExecutor, FoldListener())
wakefulnessLifecycle.addObserver(this)
// TODO(b/254878364): remove this call to NPVC.getView()
- getShadeFoldAnimator().view.repeatWhenAttached {
+ getShadeFoldAnimator().view?.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) }
}
}
@@ -128,7 +133,7 @@ constructor(
}
private fun getShadeFoldAnimator(): ShadeFoldAnimator =
- centralSurfaces.shadeViewController.shadeFoldAnimator
+ shadeViewController.shadeFoldAnimator
private fun setAnimationState(playing: Boolean) {
shouldPlayAnimation = playing
@@ -161,10 +166,9 @@ constructor(
// but we should wait for the initial animation preparations to be drawn
// (setting initial alpha/translation)
// TODO(b/254878364): remove this call to NPVC.getView()
- OneShotPreDrawListener.add(
- getShadeFoldAnimator().view,
- onReady
- )
+ getShadeFoldAnimator().view?.let {
+ OneShotPreDrawListener.add(it, onReady)
+ }
} else {
// No animation, call ready callback immediately
onReady.run()
diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
index db2aca873d0c..65a02184f96d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
@@ -16,32 +16,34 @@
package com.android.systemui.util
-import android.app.WallpaperInfo
import android.app.WallpaperManager
import android.util.Log
import android.view.View
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository
import javax.inject.Inject
import kotlin.math.max
private const val TAG = "WallpaperController"
+/**
+ * Controller for wallpaper-related logic.
+ *
+ * Note: New logic should be added to [WallpaperRepository], not this class.
+ */
@SysUISingleton
-class WallpaperController @Inject constructor(private val wallpaperManager: WallpaperManager) {
+class WallpaperController @Inject constructor(
+ private val wallpaperManager: WallpaperManager,
+ private val wallpaperRepository: WallpaperRepository,
+) {
var rootView: View? = null
private var notificationShadeZoomOut: Float = 0f
private var unfoldTransitionZoomOut: Float = 0f
- private var wallpaperInfo: WallpaperInfo? = null
-
- fun onWallpaperInfoUpdated(wallpaperInfo: WallpaperInfo?) {
- this.wallpaperInfo = wallpaperInfo
- }
-
private val shouldUseDefaultUnfoldTransition: Boolean
- get() = wallpaperInfo?.shouldUseDefaultUnfoldTransition()
+ get() = wallpaperRepository.wallpaperInfo.value?.shouldUseDefaultUnfoldTransition()
?: true
fun setNotificationShadeZoom(zoomOut: Float) {
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
index a64058968581..b45b8cd15bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
@@ -16,9 +16,11 @@
package com.android.systemui.wallpapers.data.repository
+import android.app.WallpaperInfo
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
@@ -29,5 +31,6 @@ import kotlinx.coroutines.flow.asStateFlow
*/
@SysUISingleton
class NoopWallpaperRepository @Inject constructor() : WallpaperRepository {
+ override val wallpaperInfo: StateFlow<WallpaperInfo?> = MutableStateFlow(null).asStateFlow()
override val wallpaperSupportsAmbientMode = MutableStateFlow(false).asStateFlow()
}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index 48895ffcacb9..b8f95832b852 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.wallpapers.data.repository
+import android.app.WallpaperInfo
import android.app.WallpaperManager
import android.content.Context
import android.content.Intent
@@ -36,11 +37,15 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
/** A repository storing information about the current wallpaper. */
interface WallpaperRepository {
+ /** Emits the current user's current wallpaper. */
+ val wallpaperInfo: StateFlow<WallpaperInfo?>
+
/** Emits true if the current user's current wallpaper supports ambient mode. */
val wallpaperSupportsAmbientMode: StateFlow<Boolean>
}
@@ -78,28 +83,35 @@ constructor(
// Only update the wallpaper status once the user selection has finished.
.filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
- override val wallpaperSupportsAmbientMode: StateFlow<Boolean> =
+ override val wallpaperInfo: StateFlow<WallpaperInfo?> =
if (!wallpaperManager.isWallpaperSupported || !deviceSupportsAodWallpaper) {
- MutableStateFlow(false).asStateFlow()
+ MutableStateFlow(null).asStateFlow()
} else {
combine(wallpaperChanged, selectedUser) { _, selectedUser ->
- doesWallpaperSupportAmbientMode(selectedUser)
+ getWallpaper(selectedUser)
}
.stateIn(
scope,
// Always be listening for wallpaper changes.
SharingStarted.Eagerly,
- initialValue =
- doesWallpaperSupportAmbientMode(userRepository.selectedUser.value),
+ initialValue = getWallpaper(userRepository.selectedUser.value),
)
}
- private fun doesWallpaperSupportAmbientMode(selectedUser: SelectedUserModel): Boolean {
- return wallpaperManager
- .getWallpaperInfoForUser(
- selectedUser.userInfo.id,
+ override val wallpaperSupportsAmbientMode: StateFlow<Boolean> =
+ wallpaperInfo
+ .map {
+ // If WallpaperInfo is null, it's ImageWallpaper which never supports ambient mode.
+ it?.supportsAmbientMode() == true
+ }
+ .stateIn(
+ scope,
+ // Always be listening for wallpaper changes.
+ SharingStarted.Eagerly,
+ initialValue = wallpaperInfo.value?.supportsAmbientMode() == true,
)
- // If WallpaperInfo is null, it's ImageWallpaper which never supports ambient mode.
- ?.supportsAmbientMode() == true
+
+ private fun getWallpaper(selectedUser: SelectedUserModel): WallpaperInfo? {
+ return wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index a5365fbc3d5d..2acd4b92c5a2 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -621,13 +621,9 @@ public class BubblesManager {
}
private boolean isDismissableFromBubbles(NotificationEntry e) {
- if (mNotifPipelineFlags.allowDismissOngoing()) {
- // Bubbles are only accessible from the unlocked state,
- // so we can calculate this from the Notification flags only.
- return e.isDismissableForState(/*isLocked=*/ false);
- } else {
- return e.legacyIsDismissableRecursive();
- }
+ // Bubbles are only accessible from the unlocked state,
+ // so we can calculate this from the Notification flags only.
+ return e.isDismissableForState(/*isLocked=*/ false);
}
private boolean shouldBubbleUp(NotificationEntry e) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index 5d75428b8fb4..cb182297eae1 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -76,7 +76,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
private lateinit var mKeyguardMessageAreaController:
KeyguardMessageAreaController<BouncerKeyguardMessageArea>
- @Mock private lateinit var mPostureController: DevicePostureController
+ @Mock private lateinit var mPostureController: DevicePostureController
private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController
private lateinit var fakeFeatureFlags: FakeFeatureFlags
@@ -119,7 +119,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
mKeyguardPatternViewController.onViewAttached()
- assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline())
+ assertThat(getPatternTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
}
@Test
@@ -131,15 +131,20 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
mKeyguardPatternViewController.onViewAttached()
// Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
- assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline())
+ assertThat(getPatternTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
// Simulate posture change to state DEVICE_POSTURE_OPENED with callback
verify(mPostureController).addCallback(postureCallbackCaptor.capture())
val postureCallback: DevicePostureController.Callback = postureCallbackCaptor.value
postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
- // Verify view is now in posture state DEVICE_POSTURE_OPENED
- assertThat(getPatternTopGuideline()).isNotEqualTo(getExpectedTopGuideline())
+ // Simulate posture change to same state with callback
+ assertThat(getPatternTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
+
+ postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+ // Verify view is still in posture state DEVICE_POSTURE_OPENED
+ assertThat(getPatternTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
}
private fun getPatternTopGuideline(): Float {
@@ -150,7 +155,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() {
return cs.getConstraint(R.id.pattern_top_guideline).layout.guidePercent
}
- private fun getExpectedTopGuideline(): Float {
+ private fun getHalfOpenedBouncerHeightRatio(): Float {
return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index d256ee163877..4dc7652f83cf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -19,6 +19,8 @@ package com.android.keyguard
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.internal.widget.LockPatternUtils
@@ -32,6 +34,8 @@ import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.policy.DevicePostureController
import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -51,7 +55,10 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class KeyguardPinViewControllerTest : SysuiTestCase() {
- @Mock private lateinit var keyguardPinView: KeyguardPINView
+
+ private lateinit var objectKeyguardPINView: KeyguardPINView
+
+ @Mock private lateinit var mockKeyguardPinView: KeyguardPINView
@Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea
@@ -83,64 +90,73 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
@Mock lateinit var deleteButton: NumPadButton
@Mock lateinit var enterButton: View
- private lateinit var pinViewController: KeyguardPinViewController
-
@Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- Mockito.`when`(keyguardPinView.requireViewById<View>(R.id.bouncer_message_area))
+ Mockito.`when`(mockKeyguardPinView.requireViewById<View>(R.id.bouncer_message_area))
.thenReturn(keyguardMessageArea)
Mockito.`when`(
keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java))
)
.thenReturn(keyguardMessageAreaController)
- `when`(keyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry)
- `when`(keyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry))
+ `when`(mockKeyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry)
+ `when`(mockKeyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry))
.thenReturn(passwordTextView)
- `when`(keyguardPinView.resources).thenReturn(context.resources)
- `when`(keyguardPinView.findViewById<NumPadButton>(R.id.delete_button))
+ `when`(mockKeyguardPinView.resources).thenReturn(context.resources)
+ `when`(mockKeyguardPinView.findViewById<NumPadButton>(R.id.delete_button))
.thenReturn(deleteButton)
- `when`(keyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton)
+ `when`(mockKeyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton)
// For posture tests:
- `when`(keyguardPinView.buttons).thenReturn(arrayOf())
+ `when`(mockKeyguardPinView.buttons).thenReturn(arrayOf())
`when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
- pinViewController =
- KeyguardPinViewController(
- keyguardPinView,
- keyguardUpdateMonitor,
- securityMode,
- lockPatternUtils,
- mKeyguardSecurityCallback,
- keyguardMessageAreaControllerFactory,
- mLatencyTracker,
- liftToActivateListener,
- mEmergencyButtonController,
- falsingCollector,
- postureController,
- featureFlags
- )
+ objectKeyguardPINView =
+ View.inflate(mContext, R.layout.keyguard_pin_view, null)
+ .findViewById(R.id.keyguard_pin_view) as KeyguardPINView
+ }
+
+ private fun constructPinViewController(
+ mKeyguardPinView: KeyguardPINView
+ ): KeyguardPinViewController {
+ return KeyguardPinViewController(
+ mKeyguardPinView,
+ keyguardUpdateMonitor,
+ securityMode,
+ lockPatternUtils,
+ mKeyguardSecurityCallback,
+ keyguardMessageAreaControllerFactory,
+ mLatencyTracker,
+ liftToActivateListener,
+ mEmergencyButtonController,
+ falsingCollector,
+ postureController,
+ featureFlags
+ )
}
@Test
- fun onViewAttached_deviceHalfFolded_propagatedToPinView() {
- `when`(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
+ fun onViewAttached_deviceHalfFolded_propagatedToPatternView() {
+ val pinViewController = constructPinViewController(objectKeyguardPINView)
+ overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
+ whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
pinViewController.onViewAttached()
- verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED)
+ assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
}
@Test
- fun onDevicePostureChanged_deviceHalfFolded_propagatedToPinView() {
- `when`(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
+ fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() {
+ val pinViewController = constructPinViewController(objectKeyguardPINView)
+ overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
- // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
+ whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
pinViewController.onViewAttached()
- verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED)
+ // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
+ assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
// Simulate posture change to state DEVICE_POSTURE_OPENED with callback
verify(postureController).addCallback(postureCallbackCaptor.capture())
@@ -148,31 +164,57 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
// Verify view is now in posture state DEVICE_POSTURE_OPENED
- verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_OPENED)
+ assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
+
+ // Simulate posture change to same state with callback
+ postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
+
+ // Verify view is still in posture state DEVICE_POSTURE_OPENED
+ assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
+ }
+
+ private fun getPinTopGuideline(): Float {
+ val cs = ConstraintSet()
+ val container = objectKeyguardPINView.findViewById(R.id.pin_container) as ConstraintLayout
+ cs.clone(container)
+ return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent
+ }
+
+ private fun getHalfOpenedBouncerHeightRatio(): Float {
+ return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
}
@Test
fun startAppearAnimation() {
+ val pinViewController = constructPinViewController(mockKeyguardPinView)
+
pinViewController.startAppearAnimation()
+
verify(keyguardMessageAreaController)
.setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
}
@Test
fun startAppearAnimation_withExistingMessage() {
+ val pinViewController = constructPinViewController(mockKeyguardPinView)
Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
+
pinViewController.startAppearAnimation()
+
verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
}
@Test
fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
+ val pinViewController = constructPinViewController(mockKeyguardPinView)
`when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
+ `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
`when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
`when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3)
`when`(passwordTextView.text).thenReturn("")
pinViewController.startAppearAnimation()
+
verify(deleteButton).visibility = View.INVISIBLE
verify(enterButton).visibility = View.INVISIBLE
verify(passwordTextView).setUsePinShapes(true)
@@ -181,12 +223,15 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
@Test
fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
+ val pinViewController = constructPinViewController(mockKeyguardPinView)
`when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
+ `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
`when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
`when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6)
`when`(passwordTextView.text).thenReturn("")
pinViewController.startAppearAnimation()
+
verify(deleteButton).visibility = View.VISIBLE
verify(enterButton).visibility = View.VISIBLE
verify(passwordTextView).setUsePinShapes(true)
@@ -195,7 +240,10 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
@Test
fun handleLockout_readsNumberOfErrorAttempts() {
+ val pinViewController = constructPinViewController(mockKeyguardPinView)
+
pinViewController.handleAttemptLockout(0)
+
verify(lockPatternUtils).getCurrentFailedPasswordAttempts(anyInt())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index d44717420bdf..3abae6bcd197 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -37,6 +37,7 @@ import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserS
import com.android.keyguard.KeyguardSecurityModel.SecurityMode
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate
import com.android.systemui.biometrics.SideFpsController
import com.android.systemui.biometrics.SideFpsUiRequestSource
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants
@@ -123,6 +124,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
@Mock private lateinit var viewMediatorCallback: ViewMediatorCallback
@Mock private lateinit var audioManager: AudioManager
@Mock private lateinit var userInteractor: UserInteractor
+ @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
@Captor
private lateinit var swipeListenerArgumentCaptor:
@@ -224,6 +226,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
mock(),
{ JavaAdapter(sceneTestUtils.testScope.backgroundScope) },
userInteractor,
+ faceAuthAccessibilityDelegate,
) {
sceneInteractor
}
@@ -244,6 +247,11 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() {
}
@Test
+ fun setAccessibilityDelegate() {
+ verify(view).accessibilityDelegate = eq(faceAuthAccessibilityDelegate)
+ }
+
+ @Test
fun showSecurityScreen_canInflateAllModes() {
val modes = SecurityMode.values()
for (mode in modes) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 512e5dc1a0d6..7114c22b5701 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -27,6 +27,7 @@ import android.testing.AndroidTestingRunner;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ClockConfig;
import com.android.systemui.plugins.ClockController;
@@ -62,6 +63,8 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase {
@Mock private FeatureFlags mFeatureFlags;
@Mock private InteractionJankMonitor mInteractionJankMonitor;
+ @Mock private DumpManager mDumpManager;
+
@Captor
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
@@ -82,7 +85,8 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase {
mScreenOffAnimationController,
mKeyguardLogger,
mFeatureFlags,
- mInteractionJankMonitor) {
+ mInteractionJankMonitor,
+ mDumpManager) {
@Override
void setProperty(
AnimatableProperty property,
@@ -170,4 +174,12 @@ public class KeyguardStatusViewControllerTest extends SysuiTestCase {
verify(mKeyguardClockSwitchController, times(1)).setSplitShadeEnabled(false);
verify(mKeyguardClockSwitchController, times(0)).setSplitShadeEnabled(true);
}
+
+ @Test
+ public void correctlyDump() {
+ mController.onInit();
+ verify(mDumpManager).registerDumpable(mController);
+ mController.onDestroy();
+ verify(mDumpManager, times(1)).unregisterDumpable(KeyguardStatusViewController.TAG);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
index 6decb88ee148..5867a40c78fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
@@ -30,6 +30,8 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.NotificationMediaManager;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -43,12 +45,14 @@ import org.junit.runner.RunWith;
@RunWithLooper
public class ExpandHelperTest extends SysuiTestCase {
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private ExpandableNotificationRow mRow;
private ExpandHelper mExpandHelper;
private ExpandHelper.Callback mCallback;
@Before
public void setUp() throws Exception {
+ mFeatureFlags.setDefault(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION);
mDependency.injectMockDependency(KeyguardUpdateMonitor.class);
mDependency.injectMockDependency(NotificationMediaManager.class);
allowTestableLooperAsMainThread();
@@ -56,7 +60,8 @@ public class ExpandHelperTest extends SysuiTestCase {
NotificationTestHelper helper = new NotificationTestHelper(
mContext,
mDependency,
- TestableLooper.get(this));
+ TestableLooper.get(this),
+ mFeatureFlags);
mRow = helper.createRow();
mCallback = mock(ExpandHelper.Callback.class);
mExpandHelper = new ExpandHelper(context, mCallback, 10, 100);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
index 025c88c36203..576f689a16d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java
@@ -39,6 +39,7 @@ import com.android.systemui.recents.Recents;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -65,6 +66,8 @@ public class SystemActionsTest extends SysuiTestCase {
@Mock
private ShadeController mShadeController;
@Mock
+ private ShadeViewController mShadeViewController;
+ @Mock
private Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@Mock
private Optional<Recents> mRecentsOptional;
@@ -82,7 +85,8 @@ public class SystemActionsTest extends SysuiTestCase {
mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
mContext.addMockSystemService(InputManager.class, mInputManager);
mSystemActions = new SystemActions(mContext, mUserTracker, mNotificationShadeController,
- mShadeController, mCentralSurfacesOptionalLazy, mRecentsOptional, mDisplayTracker);
+ mShadeController, () -> mShadeViewController, mCentralSurfacesOptionalLazy,
+ mRecentsOptional, mDisplayTracker);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
index d5e6881500bc..7b99314692b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
@@ -15,11 +15,13 @@
*/
package com.android.systemui.accessibility.fontscaling
+import android.content.res.Configuration
import android.os.Handler
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.ViewGroup
+import android.widget.Button
import android.widget.SeekBar
import androidx.test.filters.SmallTest
import com.android.systemui.R
@@ -61,6 +63,7 @@ class FontScalingDialogTest : SysuiTestCase() {
private lateinit var secureSettings: SecureSettings
private lateinit var systemClock: FakeSystemClock
private lateinit var backgroundDelayableExecutor: FakeExecutor
+ private lateinit var testableLooper: TestableLooper
private val fontSizeValueArray: Array<String> =
mContext
.getResources()
@@ -73,7 +76,8 @@ class FontScalingDialogTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- val mainHandler = Handler(TestableLooper.get(this).getLooper())
+ testableLooper = TestableLooper.get(this)
+ val mainHandler = Handler(testableLooper.looper)
systemSettings = FakeSettings()
// Guarantee that the systemSettings always starts with the default font scale.
systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
@@ -286,4 +290,26 @@ class FontScalingDialogTest : SysuiTestCase() {
verify(fontScalingDialog).createTextPreview(/* index= */ 0)
fontScalingDialog.dismiss()
}
+
+ @Test
+ fun changeFontSize_buttonIsDisabledBeforeFontSizeChangeFinishes() {
+ fontScalingDialog.show()
+
+ val iconEndFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_end_frame)!!
+ val doneButton: Button = fontScalingDialog.findViewById(com.android.internal.R.id.button1)!!
+
+ iconEndFrame.performClick()
+ backgroundDelayableExecutor.runAllReady()
+ backgroundDelayableExecutor.advanceClockToNext()
+ backgroundDelayableExecutor.runAllReady()
+
+ // Verify that the button is disabled before receiving onConfigurationChanged
+ assertThat(doneButton.isEnabled).isFalse()
+
+ val config = Configuration()
+ config.fontScale = 1.15f
+ fontScalingDialog.onConfigurationChanged(config)
+ testableLooper.processAllMessages()
+ assertThat(doneButton.isEnabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index da9ceb47446a..212dad78d5b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.children
import junit.framework.Assert.assertEquals
@@ -19,7 +20,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import com.android.app.animation.Interpolators
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -178,7 +178,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
}
@Test
- fun animatesRootAndChildren() {
+ fun animatesRootAndChildren_withoutExcludedViews() {
setUpRootWithChildren()
val success = ViewHierarchyAnimator.animate(rootView)
@@ -208,6 +208,40 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
}
@Test
+ fun animatesRootAndChildren_withExcludedViews() {
+ setUpRootWithChildren()
+
+ val success = ViewHierarchyAnimator.animate(
+ rootView,
+ excludedViews = setOf(rootView.getChildAt(0))
+ )
+ // Change all bounds.
+ rootView.measure(
+ View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY)
+ )
+ rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */)
+
+ assertTrue(success)
+ assertNotNull(rootView.getTag(R.id.tag_animator))
+ assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
+ assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
+ // The initial values for the affected views should be those of the previous layout, while
+ // the excluded view should be at the final values from the beginning.
+ checkBounds(rootView, l = 0, t = 0, r = 200, b = 100)
+ checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
+ checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100)
+ endAnimation(rootView)
+ assertNull(rootView.getTag(R.id.tag_animator))
+ assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator))
+ assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator))
+ // The end values should be those of the latest layout.
+ checkBounds(rootView, l = 10, t = 20, r = 200, b = 120)
+ checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100)
+ checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100)
+ }
+
+ @Test
fun animatesInvisibleViews() {
rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */)
rootView.visibility = View.INVISIBLE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
new file mode 100644
index 000000000000..005697044c0f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.authentication.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class AuthenticationRepositoryTest : SysuiTestCase() {
+
+ @Mock private lateinit var lockPatternUtils: LockPatternUtils
+
+ private val testUtils = SceneTestUtils(this)
+ private val testScope = testUtils.testScope
+ private val userRepository = FakeUserRepository()
+
+ private lateinit var underTest: AuthenticationRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ userRepository.setUserInfos(USER_INFOS)
+ runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) }
+
+ underTest =
+ AuthenticationRepositoryImpl(
+ applicationScope = testScope.backgroundScope,
+ getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN },
+ backgroundDispatcher = testUtils.testDispatcher,
+ userRepository = userRepository,
+ keyguardRepository = testUtils.keyguardRepository,
+ lockPatternUtils = lockPatternUtils,
+ )
+ }
+
+ @Test
+ fun isAutoConfirmEnabled() =
+ testScope.runTest {
+ whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true)
+ whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false)
+
+ val values by collectValues(underTest.isAutoConfirmEnabled)
+ assertThat(values.first()).isFalse()
+ assertThat(values.last()).isTrue()
+
+ userRepository.setSelectedUserInfo(USER_INFOS[1])
+ assertThat(values.last()).isFalse()
+ }
+
+ @Test
+ fun isPatternVisible() =
+ testScope.runTest {
+ whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[0].id)).thenReturn(false)
+ whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[1].id)).thenReturn(true)
+
+ val values by collectValues(underTest.isPatternVisible)
+ assertThat(values.first()).isTrue()
+ assertThat(values.last()).isFalse()
+
+ userRepository.setSelectedUserInfo(USER_INFOS[1])
+ assertThat(values.last()).isTrue()
+ }
+
+ companion object {
+ private val USER_INFOS =
+ listOf(
+ UserInfo(
+ /* id= */ 100,
+ /* name= */ "First user",
+ /* flags= */ 0,
+ ),
+ UserInfo(
+ /* id= */ 101,
+ /* name= */ "Second user",
+ /* flags= */ 0,
+ ),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt
new file mode 100644
index 000000000000..ec17794d4ee2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.testing.TestableLooper
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.FaceAuthApiRequestReason
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class FaceAuthAccessibilityDelegateTest : SysuiTestCase() {
+
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var hostView: View
+ @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor
+ private lateinit var underTest: FaceAuthAccessibilityDelegate
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest =
+ FaceAuthAccessibilityDelegate(
+ context.resources,
+ keyguardUpdateMonitor,
+ faceAuthInteractor,
+ )
+ }
+
+ @Test
+ fun shouldListenForFaceTrue_onInitializeAccessibilityNodeInfo_clickActionAdded() {
+ whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true)
+
+ // WHEN node is initialized
+ val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+ underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+ // THEN a11y action is added
+ val argumentCaptor = argumentCaptor<AccessibilityNodeInfo.AccessibilityAction>()
+ verify(mockedNodeInfo).addAction(argumentCaptor.capture())
+
+ // AND the a11y action is a click action
+ assertEquals(
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+ argumentCaptor.value.id
+ )
+ }
+
+ @Test
+ fun shouldListenForFaceFalse_onInitializeAccessibilityNodeInfo_clickActionNotAdded() {
+ whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false)
+
+ // WHEN node is initialized
+ val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java)
+ underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo)
+
+ // THEN a11y action is NOT added
+ verify(mockedNodeInfo, never())
+ .addAction(any(AccessibilityNodeInfo.AccessibilityAction::class.java))
+ }
+
+ @Test
+ fun performAccessibilityAction_actionClick_retriesFaceAuth() {
+ whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true)
+
+ // WHEN click action is performed
+ underTest.performAccessibilityAction(
+ hostView,
+ AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
+ null
+ )
+
+ // THEN retry face auth
+ verify(keyguardUpdateMonitor)
+ .requestFaceAuth(eq(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION))
+ verify(faceAuthInteractor).onAccessibilityAction()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index eaa31ac1d157..3169b091217b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -44,27 +44,25 @@ import android.view.View
import android.view.ViewPropertyAnimator
import android.view.WindowInsets
import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
import android.view.WindowMetrics
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.airbnb.lottie.LottieAnimationView
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -101,7 +99,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
@SmallTest
@RoboPilotTest
@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
class SideFpsControllerTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@ class SideFpsControllerTest : SysuiTestCase() {
private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
private lateinit var displayStateInteractor: DisplayStateInteractor
- private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel
- private val fingerprintRepository = FakeFingerprintPropertyRepository()
private val executor = FakeExecutor(FakeSystemClock())
private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
@@ -153,8 +149,8 @@ class SideFpsControllerTest : SysuiTestCase() {
mock(KeyguardStateController::class.java),
keyguardBouncerRepository,
FakeBiometricSettingsRepository(),
- FakeDeviceEntryFingerprintAuthRepository(),
FakeSystemClock(),
+ mock(KeyguardUpdateMonitor::class.java),
)
displayStateInteractor =
DisplayStateInteractorImpl(
@@ -163,15 +159,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
rearDisplayStateRepository
)
- sideFpsOverlayViewModel =
- SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository))
-
- fingerprintRepository.setProperties(
- sensorId = 1,
- strength = SensorStrength.STRONG,
- sensorType = FingerprintSensorType.REAR,
- sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0))
- )
context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, windowManager)
@@ -278,7 +265,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
handler,
alternateBouncerInteractor,
- { sideFpsOverlayViewModel },
TestCoroutineScope(),
dumpManager
)
@@ -697,6 +683,106 @@ class SideFpsControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any())
}
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForXAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
@Test
fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
// By default all those tests assume the side fps sensor is available.
@@ -709,6 +795,51 @@ class SideFpsControllerTest : SysuiTestCase() {
assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
}
+
+ @Test
+ fun testLayoutParams_isKeyguardDialogType() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpType = overlayViewParamsCaptor.value.type
+
+ assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue()
+ }
+
+ @Test
+ fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+ assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
+ }
+
+ @Test
+ fun testLayoutParams_hasTrustedOverlayWindowFlag() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+ assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
+ }
}
private fun insetsForSmallNavbar() = insetsWithBottom(60)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 9df06dc9e18f..8dfeb3bde0c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -34,7 +34,6 @@ import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -106,8 +105,8 @@ class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest :
mock(KeyguardStateController::class.java),
keyguardBouncerRepository,
mock(BiometricSettingsRepository::class.java),
- mock(DeviceEntryFingerprintAuthRepository::class.java),
mock(SystemClock::class.java),
+ mKeyguardUpdateMonitor,
)
return createUdfpsKeyguardViewController(
/* useModernBouncer */ true, /* useExpandedOverlay */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
new file mode 100644
index 000000000000..fcc40404bf7d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.biometrics.data.repository
+
+import android.hardware.biometrics.SensorProperties
+import android.hardware.face.FaceManager
+import android.hardware.face.FaceSensorPropertiesInternal
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class FacePropertyRepositoryImplTest : SysuiTestCase() {
+ @JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ private lateinit var underTest: FacePropertyRepository
+ private lateinit var testScope: TestScope
+
+ @Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback>
+ @Mock private lateinit var faceManager: FaceManager
+ @Before
+ fun setup() {
+ testScope = TestScope()
+ underTest = createRepository(faceManager)
+ }
+
+ private fun createRepository(manager: FaceManager? = faceManager) =
+ FacePropertyRepositoryImpl(testScope.backgroundScope, manager)
+
+ @Test
+ fun whenFaceManagerIsNotPresentIsNull() =
+ testScope.runTest {
+ underTest = createRepository(null)
+ val sensor = collectLastValue(underTest.sensorInfo)
+
+ assertThat(sensor()).isNull()
+ }
+
+ @Test
+ fun providesTheValuePassedToTheAuthenticatorsRegisteredCallback() {
+ testScope.runTest {
+ val sensor by collectLastValue(underTest.sensorInfo)
+ runCurrent()
+ verify(faceManager).addAuthenticatorsRegisteredCallback(callback.capture())
+
+ callback.value.onAllAuthenticatorsRegistered(
+ listOf(createSensorProperties(1, SensorProperties.STRENGTH_STRONG))
+ )
+
+ assertThat(sensor).isEqualTo(FaceSensorInfo(1, SensorStrength.STRONG))
+ }
+ }
+
+ private fun createSensorProperties(id: Int, strength: Int) =
+ FaceSensorPropertiesInternal(id, strength, 0, emptyList(), 1, false, false, false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index ea2561594793..239e317b92f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -73,15 +73,10 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
@Test
fun initializeProperties() =
testScope.runTest {
- val sensorId by collectLastValue(repository.sensorId)
- val strength by collectLastValue(repository.strength)
- val sensorType by collectLastValue(repository.sensorType)
- val sensorLocations by collectLastValue(repository.sensorLocations)
+ val isInitialized = collectLastValue(repository.isInitialized)
- // Assert default properties.
- assertThat(sensorId).isEqualTo(-1)
- assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
- assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
+ assertDefaultProperties()
+ assertThat(isInitialized()).isFalse()
val fingerprintProps =
listOf(
@@ -120,24 +115,31 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
- assertThat(sensorId).isEqualTo(1)
- assertThat(strength).isEqualTo(SensorStrength.STRONG)
- assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
+ assertThat(repository.sensorId.value).isEqualTo(1)
+ assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
+ assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
- assertThat(sensorLocations?.size).isEqualTo(2)
- assertThat(sensorLocations).containsKey("display_id_1")
- with(sensorLocations?.get("display_id_1")!!) {
+ assertThat(repository.sensorLocations.value.size).isEqualTo(2)
+ assertThat(repository.sensorLocations.value).containsKey("display_id_1")
+ with(repository.sensorLocations.value["display_id_1"]!!) {
assertThat(displayId).isEqualTo("display_id_1")
assertThat(sensorLocationX).isEqualTo(100)
assertThat(sensorLocationY).isEqualTo(300)
assertThat(sensorRadius).isEqualTo(20)
}
- assertThat(sensorLocations).containsKey("")
- with(sensorLocations?.get("")!!) {
+ assertThat(repository.sensorLocations.value).containsKey("")
+ with(repository.sensorLocations.value[""]!!) {
assertThat(displayId).isEqualTo("")
assertThat(sensorLocationX).isEqualTo(540)
assertThat(sensorLocationY).isEqualTo(1636)
assertThat(sensorRadius).isEqualTo(130)
}
+ assertThat(isInitialized()).isTrue()
}
+
+ private fun assertDefaultProperties() {
+ assertThat(repository.sensorId.value).isEqualTo(-1)
+ assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE)
+ assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
index 896f9b114679..fd96cf45504b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
@@ -22,7 +22,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -52,9 +51,8 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
}
@Test
- fun testGetOverlayoffsets() =
+ fun testGetOverlayOffsets() =
testScope.runTest {
- // Arrange.
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
)
)
- // Act.
- val offsets by collectLastValue(interactor.overlayOffsets)
- val displayId by collectLastValue(interactor.displayId)
+ var offsets = interactor.getOverlayOffsets("display_id_1")
+ assertThat(offsets.displayId).isEqualTo("display_id_1")
+ assertThat(offsets.sensorLocationX).isEqualTo(100)
+ assertThat(offsets.sensorLocationY).isEqualTo(300)
+ assertThat(offsets.sensorRadius).isEqualTo(20)
- // Assert offsets of empty displayId.
- assertThat(displayId).isEqualTo("")
- assertThat(offsets?.displayId).isEqualTo("")
- assertThat(offsets?.sensorLocationX).isEqualTo(540)
- assertThat(offsets?.sensorLocationY).isEqualTo(1636)
- assertThat(offsets?.sensorRadius).isEqualTo(130)
-
- // Offsets should be updated correctly.
- interactor.changeDisplay("display_id_1")
- assertThat(displayId).isEqualTo("display_id_1")
- assertThat(offsets?.displayId).isEqualTo("display_id_1")
- assertThat(offsets?.sensorLocationX).isEqualTo(100)
- assertThat(offsets?.sensorLocationY).isEqualTo(300)
- assertThat(offsets?.sensorRadius).isEqualTo(20)
-
- // Should return default offset when the displayId is invalid.
- interactor.changeDisplay("invalid_display_id")
- assertThat(displayId).isEqualTo("invalid_display_id")
- assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId)
- assertThat(offsets?.sensorLocationX)
- .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX)
- assertThat(offsets?.sensorLocationY)
- .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY)
- assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius)
+ offsets = interactor.getOverlayOffsets("invalid_display_id")
+ assertThat(offsets.displayId).isEqualTo("")
+ assertThat(offsets.sensorLocationX).isEqualTo(540)
+ assertThat(offsets.sensorLocationY).isEqualTo(1636)
+ assertThat(offsets.sensorRadius).isEqualTo(130)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt
new file mode 100644
index 000000000000..f9b590f8e018
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptHistoryImplTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.shared.model.BiometricModality
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class PromptHistoryImplTest : SysuiTestCase() {
+
+ private lateinit var history: PromptHistoryImpl
+
+ @Before
+ fun setup() {
+ history = PromptHistoryImpl()
+ }
+
+ @Test
+ fun empty() {
+ assertThat(history.faceFailed).isFalse()
+ assertThat(history.fingerprintFailed).isFalse()
+ }
+
+ @Test
+ fun faceFailed() =
+ repeat(2) {
+ history.failure(BiometricModality.None)
+ history.failure(BiometricModality.Face)
+
+ assertThat(history.faceFailed).isTrue()
+ assertThat(history.fingerprintFailed).isFalse()
+ }
+
+ @Test
+ fun fingerprintFailed() =
+ repeat(2) {
+ history.failure(BiometricModality.None)
+ history.failure(BiometricModality.Fingerprint)
+
+ assertThat(history.faceFailed).isFalse()
+ assertThat(history.fingerprintFailed).isTrue()
+ }
+
+ @Test
+ fun coexFailed() =
+ repeat(2) {
+ history.failure(BiometricModality.Face)
+ history.failure(BiometricModality.Fingerprint)
+
+ assertThat(history.faceFailed).isTrue()
+ assertThat(history.fingerprintFailed).isTrue()
+
+ history.failure(BiometricModality.None)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 91140a9b0fc4..40b1f207894a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -335,7 +335,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
error,
messageAfterError = "or me",
authenticateAfterError = false,
- suppressIf = { _ -> true },
+ suppressIf = { _, _ -> true },
)
}
}
@@ -364,7 +364,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa
error,
messageAfterError = "$error $afterSuffix",
authenticateAfterError = false,
- suppressIf = { currentMessage -> suppress && currentMessage.isError },
+ suppressIf = { currentMessage, _ -> suppress && currentMessage.isError },
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
deleted file mode 100644
index a8593216e22a..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui.viewmodel
-
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.display.DisplayManagerGlobal
-import android.view.Display
-import android.view.DisplayAdjustments
-import android.view.DisplayInfo
-import android.view.Surface
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-import org.mockito.junit.MockitoJUnit
-
-private const val DISPLAY_ID = 2
-
-@SmallTest
-@RunWith(JUnit4::class)
-class SideFpsOverlayViewModelTest : SysuiTestCase() {
-
- @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
- private var testScope: TestScope = TestScope(StandardTestDispatcher())
-
- private val fingerprintRepository = FakeFingerprintPropertyRepository()
- private lateinit var interactor: SideFpsOverlayInteractor
- private lateinit var viewModel: SideFpsOverlayViewModel
-
- enum class DeviceConfig {
- X_ALIGNED,
- Y_ALIGNED,
- }
-
- private lateinit var deviceConfig: DeviceConfig
- private lateinit var indicatorBounds: Rect
- private lateinit var displayBounds: Rect
- private lateinit var sensorLocation: SensorLocationInternal
- private var displayWidth: Int = 0
- private var displayHeight: Int = 0
- private var boundsWidth: Int = 0
- private var boundsHeight: Int = 0
-
- @Before
- fun setup() {
- interactor = SideFpsOverlayInteractorImpl(fingerprintRepository)
-
- fingerprintRepository.setProperties(
- sensorId = 1,
- strength = SensorStrength.STRONG,
- sensorType = FingerprintSensorType.REAR,
- sensorLocations =
- mapOf(
- "" to
- SensorLocationInternal(
- "" /* displayId */,
- 540 /* sensorLocationX */,
- 1636 /* sensorLocationY */,
- 130 /* sensorRadius */
- ),
- "display_id_1" to
- SensorLocationInternal(
- "display_id_1" /* displayId */,
- 100 /* sensorLocationX */,
- 300 /* sensorLocationY */,
- 20 /* sensorRadius */
- )
- )
- )
- }
-
- @Test
- fun testOverlayOffsets() =
- testScope.runTest {
- viewModel = SideFpsOverlayViewModel(mContext, interactor)
-
- val interactorOffsets by collectLastValue(interactor.overlayOffsets)
- val viewModelOffsets by collectLastValue(viewModel.overlayOffsets)
-
- assertThat(viewModelOffsets).isEqualTo(interactorOffsets)
- }
-
- private fun testWithDisplay(
- deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
- isReverseDefaultRotation: Boolean = false,
- initInfo: DisplayInfo.() -> Unit = {},
- block: () -> Unit
- ) {
- this.deviceConfig = deviceConfig
-
- when (deviceConfig) {
- DeviceConfig.X_ALIGNED -> {
- displayWidth = 3000
- displayHeight = 1500
- sensorLocation = SensorLocationInternal("", 2500, 0, 0)
- boundsWidth = 200
- boundsHeight = 100
- }
- DeviceConfig.Y_ALIGNED -> {
- displayWidth = 2500
- displayHeight = 2000
- sensorLocation = SensorLocationInternal("", 0, 300, 0)
- boundsWidth = 100
- boundsHeight = 200
- }
- }
-
- indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
- displayBounds = Rect(0, 0, displayWidth, displayHeight)
-
- val displayInfo = DisplayInfo()
- displayInfo.initInfo()
-
- val dmGlobal = Mockito.mock(DisplayManagerGlobal::class.java)
- val display =
- Display(
- dmGlobal,
- DISPLAY_ID,
- displayInfo,
- DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
- )
-
- whenever(dmGlobal.getDisplayInfo(ArgumentMatchers.eq(DISPLAY_ID))).thenReturn(displayInfo)
-
- val sideFpsOverlayViewModelContext =
- context.createDisplayContext(display) as SysuiTestableContext
- sideFpsOverlayViewModelContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_reverseDefaultRotation,
- isReverseDefaultRotation
- )
- viewModel = SideFpsOverlayViewModel(sideFpsOverlayViewModelContext, interactor)
-
- block()
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
- * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
- * placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForXAlignedSensor_0() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
- isReverseDefaultRotation = false,
- { rotation = Surface.ROTATION_0 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- val displayInfo: DisplayInfo = DisplayInfo()
- context.display!!.getDisplayInfo(displayInfo)
- assertThat(displayInfo.rotation).isEqualTo(Surface.ROTATION_0)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left)
- .isEqualTo(sensorLocation.sensorLocationX)
- assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
- }
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
- * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
- * works correctly, tests for indicator placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.X_ALIGNED,
- isReverseDefaultRotation = true,
- { rotation = Surface.ROTATION_270 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left)
- .isEqualTo(sensorLocation.sensorLocationX)
- assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
- }
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
- * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
- * placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForYAlignedSensor_0() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED,
- isReverseDefaultRotation = false,
- { rotation = Surface.ROTATION_0 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
- assertThat(viewModel.sensorBounds.value.top)
- .isEqualTo(sensorLocation.sensorLocationY)
- }
- }
-
- /**
- * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
- * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
- * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
- * works correctly, tests for indicator placement in other rotations have been omitted.
- */
- @Test
- fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
- testScope.runTest {
- testWithDisplay(
- deviceConfig = DeviceConfig.Y_ALIGNED,
- isReverseDefaultRotation = true,
- { rotation = Surface.ROTATION_270 }
- ) {
- viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
-
- assertThat(viewModel.sensorBounds.value).isNotNull()
- assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
- assertThat(viewModel.sensorBounds.value.top)
- .isEqualTo(sensorLocation.sensorLocationY)
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
index 37b9ca49ef57..186df02536ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.bouncer.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
@@ -54,6 +55,7 @@ class AlternateBouncerInteractorTest : SysuiTestCase() {
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var systemClock: SystemClock
@Mock private lateinit var bouncerLogger: TableLogBuffer
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Before
fun setup() {
@@ -72,8 +74,8 @@ class AlternateBouncerInteractorTest : SysuiTestCase() {
keyguardStateController,
bouncerRepository,
biometricSettingsRepository,
- deviceEntryFingerprintAuthRepository,
systemClock,
+ keyguardUpdateMonitor,
)
}
@@ -118,7 +120,7 @@ class AlternateBouncerInteractorTest : SysuiTestCase() {
@Test
fun canShowAlternateBouncerForFingerprint_fingerprintLockedOut() {
givenCanShowAlternateBouncer()
- deviceEntryFingerprintAuthRepository.setLockedOut(true)
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
assertFalse(underTest.canShowAlternateBouncerForFingerprint())
}
@@ -168,7 +170,7 @@ class AlternateBouncerInteractorTest : SysuiTestCase() {
biometricSettingsRepository.setFingerprintEnrolled(true)
biometricSettingsRepository.setStrongBiometricAllowed(true)
biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
- deviceEntryFingerprintAuthRepository.setLockedOut(false)
+ whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
whenever(keyguardStateController.isUnlocked).thenReturn(false)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 45d1af722369..8edc6cf8dd54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -29,6 +29,7 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -77,7 +78,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
SceneTestUtils.CONTAINER_1,
@@ -88,7 +89,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown()
assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN)
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -98,7 +99,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -112,8 +113,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(1)
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(1)
- assertThat(entries?.map { it.input }).containsExactly(1)
+ assertThat(pin).containsExactly(1)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -123,7 +123,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -134,12 +134,12 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onShown()
runCurrent()
underTest.onPinButtonClicked(1)
- assertThat(entries).hasSize(1)
+ assertThat(pin).hasSize(1)
underTest.onBackspaceButtonClicked()
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -148,7 +148,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
testScope.runTest {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -166,9 +166,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(4)
underTest.onPinButtonClicked(5)
- assertThat(entries).hasSize(3)
- assertThat(entries?.map { it.input }).containsExactly(1, 4, 5).inOrder()
- assertThat(entries?.map { it.sequenceNumber }).isInStrictOrder()
+ assertThat(pin).containsExactly(1, 4, 5).inOrder()
}
@Test
@@ -177,7 +175,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -195,7 +193,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onBackspaceButtonLongPressed()
assertThat(message?.text).isEmpty()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -227,7 +225,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -244,7 +242,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onAuthenticateButtonClicked()
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@@ -255,7 +253,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
sceneInteractor.setCurrentScene(
@@ -271,7 +269,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
underTest.onPinButtonClicked(5) // PIN is now wrong!
underTest.onAuthenticateButtonClicked()
assertThat(message?.text).isEqualTo(WRONG_PIN)
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
// Enter the correct PIN:
@@ -312,7 +310,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
val currentScene by
collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
val message by collectLastValue(bouncerViewModel.message)
- val entries by collectLastValue(underTest.pinEntries)
+ val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
@@ -329,7 +327,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
FakeAuthenticationRepository.DEFAULT_PIN.last() + 1
) // PIN is now wrong!
- assertThat(entries).hasSize(0)
+ assertThat(pin).isEmpty()
assertThat(message?.text).isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
new file mode 100644
index 000000000000..4c279ea08fd7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -0,0 +1,277 @@
+package com.android.systemui.bouncer.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll
+import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit
+import com.android.systemui.bouncer.ui.viewmodel.PinInputSubject.Companion.assertThat
+import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel.Companion.empty
+import com.google.common.truth.Fact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth.assertAbout
+import com.google.common.truth.Truth.assertThat
+import java.lang.Character.isDigit
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * This test uses a mnemonic code to create and verify PinInput instances: strings of digits [0-9]
+ * for [Digit] tokens, as well as a `C` for the [ClearAll] token.
+ */
+@SmallTest
+@RunWith(JUnit4::class)
+class PinInputViewModelTest : SysuiTestCase() {
+
+ @Test
+ fun create_emptyList_throws() {
+ assertThrows(IllegalArgumentException::class.java) { PinInputViewModel(emptyList()) }
+ }
+
+ @Test
+ fun create_inputWithoutLeadingClearAll_throws() {
+ val exception =
+ assertThrows(IllegalArgumentException::class.java) {
+ PinInputViewModel(listOf(Digit(0)))
+ }
+ assertThat(exception).hasMessageThat().contains("does not begin with a ClearAll token")
+ }
+
+ @Test
+ fun create_inputNotInAscendingOrder_throws() {
+ val sentinel = ClearAll()
+ val first = Digit(0)
+ val second = Digit(1)
+ // [first] is created before [second] is created, thus their sequence numbers are ordered.
+ check(first.sequenceNumber < second.sequenceNumber)
+
+ val exception =
+ assertThrows(IllegalArgumentException::class.java) {
+ // Passing the [Digit] tokens in reverse order throws.
+ PinInputViewModel(listOf(sentinel, second, first))
+ }
+ assertThat(exception).hasMessageThat().contains("EntryTokens are not sorted")
+ }
+
+ @Test
+ fun append_digitToEmptyInput() {
+ val result = empty().append(0)
+ assertThat(result).matches("C0")
+ }
+
+ @Test
+ fun append_digitToExistingPin() {
+ val subject = pinInput("C1")
+ assertThat(subject.append(2)).matches("C12")
+ }
+
+ @Test
+ fun append_withTwoCompletePinsEntered_dropsFirst() {
+ val subject = pinInput("C12C34C")
+ assertThat(subject.append(5)).matches("C34C5")
+ }
+
+ @Test
+ fun deleteLast_removesLastDigit() {
+ val subject = pinInput("C12")
+ assertThat(subject.deleteLast()).matches("C1")
+ }
+
+ @Test
+ fun deleteLast_onEmptyInput_returnsSameInstance() {
+ val subject = empty()
+ assertThat(subject.deleteLast()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun deleteLast_onInputEndingInClearAll_returnsSameInstance() {
+ val subject = pinInput("C12C")
+ assertThat(subject.deleteLast()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun clearAll_appendsClearAllEntryToExistingInput() {
+ val subject = pinInput("C12")
+ assertThat(subject.clearAll()).matches("C12C")
+ }
+
+ @Test
+ fun clearAll_onInputEndingInClearAll_returnsSameInstance() {
+ val subject = pinInput("C12C")
+ assertThat(subject.clearAll()).isSameInstanceAs(subject)
+ }
+
+ @Test
+ fun clearAll_retainsUpToTwoPinEntries() {
+ val subject = pinInput("C12C34")
+ assertThat(subject.clearAll()).matches("C12C34C")
+ }
+
+ @Test
+ fun isEmpty_onEmptyInput_returnsTrue() {
+ val subject = empty()
+ assertThat(subject.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun isEmpty_whenLastEntryIsDigit_returnsFalse() {
+ val subject = pinInput("C1234")
+ assertThat(subject.isEmpty()).isFalse()
+ }
+
+ @Test
+ fun isEmpty_whenLastEntryIsClearAll_returnsTrue() {
+ val subject = pinInput("C1234C")
+ assertThat(subject.isEmpty()).isTrue()
+ }
+
+ @Test
+ fun getPin_onEmptyInput_returnsEmptyList() {
+ val subject = empty()
+ assertThat(subject.getPin()).isEmpty()
+ }
+
+ @Test
+ fun getPin_whenLastEntryIsDigit_returnsPin() {
+ val subject = pinInput("C1234")
+ assertThat(subject.getPin()).containsExactly(1, 2, 3, 4)
+ }
+
+ @Test
+ fun getPin_withMultiplePins_returnsLastPin() {
+ val subject = pinInput("C1234C5678")
+ assertThat(subject.getPin()).containsExactly(5, 6, 7, 8)
+ }
+
+ @Test
+ fun getPin_whenLastEntryIsClearAll_returnsEmptyList() {
+ val subject = pinInput("C1234C")
+ assertThat(subject.getPin()).isEmpty()
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_onEmptyInput_returnsSentinel() {
+ val subject = empty()
+ val sentinel = subject.input[0] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_whenLastEntryIsDigit_returnsSentinel() {
+ val subject = pinInput("C1234")
+ val sentinel = subject.input[0] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_withMultiplePins_returnsLastMarker() {
+ val subject = pinInput("C1234C5678")
+ val lastMarker = subject.input[5] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastMarker)
+ }
+
+ @Test
+ fun mostRecentClearAllMarker_whenLastEntryIsClearAll_returnsLastEntry() {
+ val subject = pinInput("C1234C")
+ val lastEntry = subject.input[5] as ClearAll
+
+ assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastEntry)
+ }
+
+ @Test
+ fun getDigits_invalidClearAllMarker_onEmptyInput_returnsEmptyList() {
+ val subject = empty()
+ assertThat(subject.getDigits(ClearAll())).isEmpty()
+ }
+
+ @Test
+ fun getDigits_invalidClearAllMarker_whenLastEntryIsDigit_returnsEmptyList() {
+ val subject = pinInput("C1234")
+ assertThat(subject.getDigits(ClearAll())).isEmpty()
+ }
+
+ @Test
+ fun getDigits_clearAllMarkerPointsToFirstPin_returnsFirstPinDigits() {
+ val subject = pinInput("C1234C5678")
+ val marker = subject.input[0] as ClearAll
+
+ assertThat(subject.getDigits(marker).map { it.input }).containsExactly(1, 2, 3, 4)
+ }
+
+ @Test
+ fun getDigits_clearAllMarkerPointsToLastPin_returnsLastPinDigits() {
+ val subject = pinInput("C1234C5678")
+ val marker = subject.input[5] as ClearAll
+
+ assertThat(subject.getDigits(marker).map { it.input }).containsExactly(5, 6, 7, 8)
+ }
+
+ @Test
+ fun entryToken_equality() {
+ val clearAll = ClearAll()
+ val zero = Digit(0)
+ val one = Digit(1)
+
+ // Guava's EqualsTester is not available in this codebase.
+ assertThat(zero.equals(zero.copy())).isTrue()
+
+ assertThat(zero.equals(one)).isFalse()
+ assertThat(zero.equals(clearAll)).isFalse()
+
+ assertThat(clearAll.equals(clearAll.copy())).isTrue()
+ assertThat(clearAll.equals(zero)).isFalse()
+
+ // Not equal when the sequence number does not match
+ assertThat(zero.equals(Digit(0))).isFalse()
+ assertThat(clearAll.equals(ClearAll())).isFalse()
+ }
+
+ private fun pinInput(mnemonics: String): PinInputViewModel {
+ return PinInputViewModel(
+ mnemonics.map {
+ when {
+ it == 'C' -> ClearAll()
+ isDigit(it) -> Digit(it.digitToInt())
+ else -> throw AssertionError()
+ }
+ }
+ )
+ }
+}
+
+private class PinInputSubject
+private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) :
+ Subject(metadata, actual) {
+
+ fun matches(mnemonics: String) {
+ val actualMnemonics =
+ actual.input
+ .map { entry ->
+ when (entry) {
+ is Digit -> entry.input.digitToChar()
+ is ClearAll -> 'C'
+ else -> throw IllegalArgumentException()
+ }
+ }
+ .joinToString(separator = "")
+
+ if (mnemonics != actualMnemonics) {
+ failWithActual(
+ Fact.simpleFact(
+ "expected pin input to be '$mnemonics' but is '$actualMnemonics' instead"
+ )
+ )
+ }
+ }
+
+ companion object {
+ fun assertThat(input: PinInputViewModel): PinInputSubject =
+ assertAbout(Factory(::PinInputSubject)).that(input)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
index 2a4c0eb18d02..7628be44755d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java
@@ -126,7 +126,8 @@ public class IntentCreatorTest extends SysuiTestCase {
assertEquals(Intent.ACTION_CHOOSER, intent.getAction());
assertFlags(intent, EXTERNAL_INTENT_FLAGS);
Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
- assertEquals(uri, target.getData());
+ assertEquals(uri, target.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class));
+ assertEquals(uri, target.getClipData().getItemAt(0).getUri());
assertEquals("image/png", target.getType());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
index 461ec653d819..40f0ed3626db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationCollectionLiveDataTest.java
@@ -28,10 +28,10 @@ import androidx.lifecycle.Observer;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.DreamLogger;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.log.core.FakeLogBuffer;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -57,8 +57,6 @@ public class ComplicationCollectionLiveDataTest extends SysuiTestCase {
private FakeFeatureFlags mFeatureFlags;
@Mock
private Observer mObserver;
- @Mock
- private DreamLogger mLogger;
@Before
public void setUp() {
@@ -70,7 +68,7 @@ public class ComplicationCollectionLiveDataTest extends SysuiTestCase {
mExecutor,
/* overlayEnabled= */ true,
mFeatureFlags,
- mLogger);
+ FakeLogBuffer.Factory.Companion.create());
mLiveData = new ComplicationCollectionLiveData(mStateController);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
index a00e5456b711..57307fc84b1c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt
@@ -9,6 +9,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.complication.ComplicationHostViewController
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
+import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.mockito.argumentCaptor
@@ -47,7 +48,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() {
@Mock private lateinit var stateController: DreamOverlayStateController
@Mock private lateinit var configController: ConfigurationController
@Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel
- @Mock private lateinit var logger: DreamLogger
+ private val logBuffer = FakeLogBuffer.Factory.create()
private lateinit var controller: DreamOverlayAnimationsController
@Before
@@ -66,7 +67,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() {
DREAM_IN_COMPLICATIONS_ANIMATION_DURATION,
DREAM_IN_TRANSLATION_Y_DISTANCE,
DREAM_IN_TRANSLATION_Y_DURATION,
- logger
+ logBuffer
)
val mockView: View = mock()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 2c1ebe4121af..44a78ac3bc96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -34,6 +34,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.complication.Complication;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.core.FakeLogBuffer;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -58,8 +60,7 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase {
@Mock
private FeatureFlags mFeatureFlags;
- @Mock
- private DreamLogger mLogger;
+ private final LogBuffer mLogBuffer = FakeLogBuffer.Factory.Companion.create();
final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@@ -408,6 +409,11 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase {
}
private DreamOverlayStateController getDreamOverlayStateController(boolean overlayEnabled) {
- return new DreamOverlayStateController(mExecutor, overlayEnabled, mFeatureFlags, mLogger);
+ return new DreamOverlayStateController(
+ mExecutor,
+ overlayEnabled,
+ mFeatureFlags,
+ mLogBuffer
+ );
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 5dc0e55632fd..4e74f451932b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -48,6 +48,8 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.core.FakeLogBuffer;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
import com.android.systemui.statusbar.policy.NextAlarmController;
@@ -113,8 +115,8 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
DreamOverlayStateController mDreamOverlayStateController;
@Mock
UserTracker mUserTracker;
- @Mock
- DreamLogger mLogger;
+
+ LogBuffer mLogBuffer = FakeLogBuffer.Factory.Companion.create();
@Captor
private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
@@ -149,7 +151,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
mDreamOverlayStatusBarItemsProvider,
mDreamOverlayStateController,
mUserTracker,
- mLogger);
+ mLogBuffer);
}
@Test
@@ -293,7 +295,7 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
mDreamOverlayStatusBarItemsProvider,
mDreamOverlayStateController,
mUserTracker,
- mLogger);
+ mLogBuffer);
controller.onViewAttached();
verify(mView, never()).showIcon(
eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
index 872c0794ce64..2b9821406fea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
@@ -61,10 +61,8 @@ public class ShadeTouchHandlerTest extends SysuiTestCase {
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces),
+ mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController,
TOUCH_HEIGHT);
- when(mCentralSurfaces.getShadeViewController())
- .thenReturn(mShadeViewController);
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 666978e78f98..0ffa2d7b970e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -37,7 +37,9 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -71,6 +73,7 @@ import com.android.keyguard.KeyguardDisplayManager;
import com.android.keyguard.KeyguardSecurityView;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.keyguard.TestScopeProvider;
import com.android.keyguard.mediator.ScreenOnCoordinator;
import com.android.systemui.DejankUtils;
import com.android.systemui.SysuiTestCase;
@@ -108,9 +111,11 @@ import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.DeviceConfigProxyFake;
import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.wallpapers.data.repository.FakeWallpaperRepository;
import com.android.wm.shell.keyguard.KeyguardTransitions;
import org.junit.After;
@@ -124,6 +129,7 @@ import org.mockito.MockitoAnnotations;
import kotlinx.coroutines.CoroutineDispatcher;
import kotlinx.coroutines.flow.Flow;
+import kotlinx.coroutines.test.TestScope;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -131,6 +137,9 @@ import kotlinx.coroutines.flow.Flow;
public class KeyguardViewMediatorTest extends SysuiTestCase {
private KeyguardViewMediator mViewMediator;
+ private final TestScope mTestScope = TestScopeProvider.getTestScope();
+ private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
+
private @Mock UserTracker mUserTracker;
private @Mock DevicePolicyManager mDevicePolicyManager;
private @Mock LockPatternUtils mLockPatternUtils;
@@ -182,6 +191,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
private @Mock SecureSettings mSecureSettings;
private @Mock AlarmManager mAlarmManager;
private FakeSystemClock mSystemClock;
+ private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
private @Mock CoroutineDispatcher mDispatcher;
private @Mock DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
@@ -337,6 +347,21 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mViewMediator.setKeyguardEnabled(false);
TestableLooper.get(this).processAllMessages();
+ mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+ mUpdateMonitor.getCurrentUser());
+ mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+ final ArgumentCaptor<Runnable> animationRunnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+ animationRunnableCaptor.capture());
+
+ when(mStatusBarStateController.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ animationRunnableCaptor.getValue().run();
+
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ mViewMediator.mViewMediatorCallback.keyguardGone();
+
// Then dream should wake up
verify(mPowerManager).wakeUp(anyLong(), anyInt(),
eq("com.android.systemui:UNLOCK_DREAMING"));
@@ -687,6 +712,67 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
}
@Test
+ public void testWakeAndUnlockingOverDream() {
+ // Send signal to wake
+ mViewMediator.onWakeAndUnlocking();
+
+ // Ensure not woken up yet
+ verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+ // Verify keyguard told of authentication
+ verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+ mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+ mUpdateMonitor.getCurrentUser());
+ mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+ final ArgumentCaptor<Runnable> animationRunnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+ animationRunnableCaptor.capture());
+
+ when(mStatusBarStateController.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ animationRunnableCaptor.getValue().run();
+
+ when(mKeyguardStateController.isShowing()).thenReturn(false);
+ mViewMediator.mViewMediatorCallback.keyguardGone();
+
+ // Verify woken up now.
+ verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
+ }
+
+ @Test
+ public void testWakeAndUnlockingOverDream_signalAuthenticateIfStillShowing() {
+ // Send signal to wake
+ mViewMediator.onWakeAndUnlocking();
+
+ // Ensure not woken up yet
+ verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+
+ // Verify keyguard told of authentication
+ verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+ clearInvocations(mStatusBarKeyguardViewManager);
+ mViewMediator.mViewMediatorCallback.keyguardDonePending(true,
+ mUpdateMonitor.getCurrentUser());
+ mViewMediator.mViewMediatorCallback.readyForKeyguardDone();
+ final ArgumentCaptor<Runnable> animationRunnableCaptor =
+ ArgumentCaptor.forClass(Runnable.class);
+ verify(mStatusBarKeyguardViewManager).startPreHideAnimation(
+ animationRunnableCaptor.capture());
+
+ when(mStatusBarStateController.isDreaming()).thenReturn(true);
+ when(mStatusBarStateController.isDozing()).thenReturn(false);
+ animationRunnableCaptor.getValue().run();
+
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+
+ mViewMediator.mViewMediatorCallback.keyguardGone();
+
+
+ // Verify keyguard view controller informed of authentication again
+ verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean());
+ }
+
+ @Test
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public void testDoKeyguardWhileInteractive_resets() {
mViewMediator.setShowingLocked(true);
@@ -817,6 +903,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mKeyguardTransitions,
mInteractionJankMonitor,
mDreamOverlayStateController,
+ mJavaAdapter,
+ mWallpaperRepository,
() -> mShadeController,
() -> mNotificationShadeWindowController,
() -> mActivityLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 925ac30b99fd..05d6b99fe227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -16,6 +16,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.util.mockito.any
import com.android.systemui.utils.GlobalWindowManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
@@ -225,6 +226,9 @@ class ResourceTrimmerTest : SysuiTestCase() {
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
)
- verifyNoMoreInteractions(globalWindowManager)
+ // Memory hidden should still be called.
+ verify(globalWindowManager, times(1))
+ .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
+ verify(globalWindowManager, times(0)).trimCaches(any())
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index f9070b37ca48..c6a2fa50b446 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -162,11 +162,11 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
@Test
fun convenienceBiometricAllowedChange() =
testScope.runTest {
+ overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, false)
createBiometricSettingsRepository()
val convenienceBiometricAllowed =
collectLastValue(underTest.isNonStrongBiometricAllowed)
runCurrent()
-
onNonStrongAuthChanged(true, PRIMARY_USER_ID)
assertThat(convenienceBiometricAllowed()).isTrue()
@@ -175,6 +175,45 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
onNonStrongAuthChanged(false, PRIMARY_USER_ID)
assertThat(convenienceBiometricAllowed()).isFalse()
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_strongAuthRequiredOnBoot
+ )
+ }
+
+ @Test
+ fun whenStrongAuthRequiredAfterBoot_nonStrongBiometricNotAllowed() =
+ testScope.runTest {
+ overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, true)
+ createBiometricSettingsRepository()
+
+ val convenienceBiometricAllowed =
+ collectLastValue(underTest.isNonStrongBiometricAllowed)
+ runCurrent()
+ onNonStrongAuthChanged(true, PRIMARY_USER_ID)
+
+ assertThat(convenienceBiometricAllowed()).isFalse()
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_strongAuthRequiredOnBoot
+ )
+ }
+
+ @Test
+ fun whenStrongBiometricAuthIsNotAllowed_nonStrongBiometrics_alsoNotAllowed() =
+ testScope.runTest {
+ overrideResource(com.android.internal.R.bool.config_strongAuthRequiredOnBoot, false)
+ createBiometricSettingsRepository()
+
+ val convenienceBiometricAllowed =
+ collectLastValue(underTest.isNonStrongBiometricAllowed)
+ runCurrent()
+ onNonStrongAuthChanged(true, PRIMARY_USER_ID)
+ assertThat(convenienceBiometricAllowed()).isTrue()
+
+ onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, PRIMARY_USER_ID)
+ assertThat(convenienceBiometricAllowed()).isFalse()
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_strongAuthRequiredOnBoot
+ )
}
private fun onStrongAuthChanged(flags: Int, userId: Int) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 020c0b22ba27..01a6c64a6898 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -36,13 +36,18 @@ import com.android.internal.logging.UiEventLogger
import com.android.keyguard.FaceAuthUiEvent
import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN
import com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER
+import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
import com.android.systemui.dump.DumpManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
@@ -50,12 +55,12 @@ import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
-import com.android.systemui.keyguard.shared.model.HelpAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
+import com.android.systemui.keyguard.shared.model.HelpFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.SuccessAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.shared.model.WakeSleepReason
@@ -113,6 +118,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
@Mock private lateinit var sessionTracker: SessionTracker
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Captor
private lateinit var authenticationCallback: ArgumentCaptor<FaceManager.AuthenticationCallback>
@@ -131,8 +137,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
private lateinit var testScope: TestScope
private lateinit var fakeUserRepository: FakeUserRepository
- private lateinit var authStatus: FlowValue<AuthenticationStatus?>
- private lateinit var detectStatus: FlowValue<DetectionStatus?>
+ private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?>
+ private lateinit var detectStatus: FlowValue<FaceDetectionStatus?>
private lateinit var authRunning: FlowValue<Boolean?>
private lateinit var bypassEnabled: FlowValue<Boolean?>
private lateinit var lockedOut: FlowValue<Boolean?>
@@ -148,6 +154,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
+ private lateinit var fakeFacePropertyRepository: FakeFacePropertyRepository
private var wasAuthCancelled = false
private var wasDetectCancelled = false
@@ -175,10 +182,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
AlternateBouncerInteractor(
bouncerRepository = bouncerRepository,
biometricSettingsRepository = biometricSettingsRepository,
- deviceEntryFingerprintAuthRepository = deviceEntryFingerprintAuthRepository,
systemClock = mock(SystemClock::class.java),
keyguardStateController = FakeKeyguardStateController(),
statusBarStateController = mock(StatusBarStateController::class.java),
+ keyguardUpdateMonitor = keyguardUpdateMonitor,
)
bypassStateChangedListener =
@@ -221,6 +228,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
repository = keyguardTransitionRepository,
)
.keyguardTransitionInteractor
+ fakeFacePropertyRepository = FakeFacePropertyRepository()
return DeviceEntryFaceAuthRepositoryImpl(
mContext,
fmOverride,
@@ -242,6 +250,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthBuffer,
keyguardTransitionInteractor,
featureFlags,
+ fakeFacePropertyRepository,
dumpManager,
)
}
@@ -262,7 +271,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
val successResult = successResult()
authenticationCallback.value.onAuthenticationSucceeded(successResult)
- assertThat(authStatus()).isEqualTo(SuccessAuthenticationStatus(successResult))
+ val response = authStatus() as SuccessFaceAuthenticationStatus
+ assertThat(response.successResult).isEqualTo(successResult)
assertThat(authenticated()).isTrue()
assertThat(authRunning()).isFalse()
assertThat(canFaceAuthRun()).isFalse()
@@ -383,7 +393,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
detectionCallback.value.onFaceDetected(1, 1, true)
- assertThat(detectStatus()).isEqualTo(DetectionStatus(1, 1, true))
+ val status = detectStatus()!!
+ assertThat(status.sensorId).isEqualTo(1)
+ assertThat(status.userId).isEqualTo(1)
+ assertThat(status.isStrongBiometric).isEqualTo(true)
}
@Test
@@ -423,7 +436,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
FACE_ERROR_CANCELED,
"First auth attempt cancellation completed"
)
- val value = authStatus() as ErrorAuthenticationStatus
+ val value = authStatus() as ErrorFaceAuthenticationStatus
assertThat(value.msgId).isEqualTo(FACE_ERROR_CANCELED)
assertThat(value.msg).isEqualTo("First auth attempt cancellation completed")
@@ -449,6 +462,29 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun multipleCancelCallsShouldNotCauseMultipleCancellationStatusBeingEmitted() =
+ testScope.runTest {
+ initCollectors()
+ allPreconditionsToRunFaceAuthAreTrue()
+ val emittedValues by collectValues(underTest.authenticationStatus)
+
+ underTest.authenticate(FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER)
+ underTest.cancel()
+ advanceTimeBy(100)
+ underTest.cancel()
+
+ advanceTimeBy(DeviceEntryFaceAuthRepositoryImpl.DEFAULT_CANCEL_SIGNAL_TIMEOUT)
+ runCurrent()
+ advanceTimeBy(DeviceEntryFaceAuthRepositoryImpl.DEFAULT_CANCEL_SIGNAL_TIMEOUT)
+ runCurrent()
+
+ assertThat(emittedValues.size).isEqualTo(1)
+ assertThat(emittedValues.first())
+ .isInstanceOf(ErrorFaceAuthenticationStatus::class.java)
+ assertThat((emittedValues.first() as ErrorFaceAuthenticationStatus).msgId).isEqualTo(-1)
+ }
+
+ @Test
fun faceHelpMessagesAreIgnoredBasedOnConfig() =
testScope.runTest {
overrideResource(
@@ -465,7 +501,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg")
authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg")
- assertThat(authStatus()).isEqualTo(HelpAuthenticationStatus(9, "help msg"))
+ val response = authStatus() as HelpFaceAuthenticationStatus
+ assertThat(response.msg).isEqualTo("help msg")
+ assertThat(response.msgId).isEqualTo(response.msgId)
}
@Test
@@ -521,10 +559,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
- fun authenticateDoesNotRunWhenFpIsLockedOut() =
- testScope.runTest {
- testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) }
- }
+ fun authenticateDoesNotRunWhenFaceIsDisabled() =
+ testScope.runTest { testGatingCheckForFaceAuth { underTest.lockoutFaceAuth() } }
@Test
fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() =
@@ -561,6 +597,17 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun authenticateDoesNotRunWhenStrongBiometricIsNotAllowedAndFaceSensorIsStrong() =
+ testScope.runTest {
+ fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
+ runCurrent()
+
+ testGatingCheckForFaceAuth(isFaceStrong = true) {
+ biometricSettingsRepository.setIsStrongBiometricAllowed(false)
+ }
+ }
+
+ @Test
fun authenticateDoesNotRunWhenSecureCameraIsActive() =
testScope.runTest {
testGatingCheckForFaceAuth {
@@ -829,6 +876,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun disableFaceUnlockLocksOutFaceUnlock() =
+ testScope.runTest {
+ runCurrent()
+ initCollectors()
+ assertThat(underTest.isLockedOut.value).isFalse()
+
+ underTest.lockoutFaceAuth()
+ runCurrent()
+
+ assertThat(underTest.isLockedOut.value).isTrue()
+ }
+
+ @Test
fun detectDoesNotRunWhenFaceAuthNotSupportedInCurrentPosture() =
testScope.runTest {
testGatingCheckForDetect {
@@ -880,6 +940,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
}
@Test
+ fun detectDoesNotRunWhenStrongBiometricIsAllowedAndFaceAuthSensorStrengthIsStrong() =
+ testScope.runTest {
+ fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
+ runCurrent()
+
+ testGatingCheckForDetect(isFaceStrong = true) {
+ biometricSettingsRepository.setIsStrongBiometricAllowed(true)
+ // this shouldn't matter as face is set as a strong sensor
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ }
+ }
+
+ @Test
fun detectDoesNotRunIfUdfpsIsRunning() =
testScope.runTest {
testGatingCheckForDetect {
@@ -970,9 +1043,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsCalled()
}
- private suspend fun TestScope.testGatingCheckForFaceAuth(gatingCheckModifier: () -> Unit) {
+ private suspend fun TestScope.testGatingCheckForFaceAuth(
+ isFaceStrong: Boolean = false,
+ gatingCheckModifier: () -> Unit
+ ) {
initCollectors()
- allPreconditionsToRunFaceAuthAreTrue()
+ allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
gatingCheckModifier()
runCurrent()
@@ -981,7 +1057,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
assertThat(underTest.canRunFaceAuth.value).isFalse()
// flip the gating check back on.
- allPreconditionsToRunFaceAuthAreTrue()
+ allPreconditionsToRunFaceAuthAreTrue(isFaceStrong)
triggerFaceAuth(false)
@@ -1000,12 +1076,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
faceAuthenticateIsNotCalled()
}
- private suspend fun TestScope.testGatingCheckForDetect(gatingCheckModifier: () -> Unit) {
+ private suspend fun TestScope.testGatingCheckForDetect(
+ isFaceStrong: Boolean = false,
+ gatingCheckModifier: () -> Unit
+ ) {
initCollectors()
allPreconditionsToRunFaceAuthAreTrue()
- // This will stop face auth from running but is required to be false for detect.
- biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ if (isFaceStrong) {
+ biometricSettingsRepository.setStrongBiometricAllowed(false)
+ } else {
+ // This will stop face auth from running but is required to be false for detect.
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(false)
+ }
runCurrent()
assertThat(canFaceAuthRun()).isFalse()
@@ -1040,11 +1123,14 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
cancellationSignal.value.setOnCancelListener { wasAuthCancelled = true }
}
- private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() {
+ private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue(
+ isFaceStrong: Boolean = false
+ ) {
+ verify(faceManager, atLeastOnce())
+ .addLockoutResetCallback(faceLockoutResetCallback.capture())
biometricSettingsRepository.setFaceEnrolled(true)
biometricSettingsRepository.setIsFaceAuthEnabled(true)
fakeUserRepository.setUserSwitching(false)
- deviceEntryFingerprintAuthRepository.setLockedOut(false)
trustRepository.setCurrentUserTrusted(false)
keyguardRepository.setKeyguardGoingAway(false)
keyguardRepository.setWakefulnessModel(
@@ -1054,10 +1140,15 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() {
WakeSleepReason.OTHER
)
)
- biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
+ if (isFaceStrong) {
+ biometricSettingsRepository.setStrongBiometricAllowed(true)
+ } else {
+ biometricSettingsRepository.setIsNonStrongBiometricAllowed(true)
+ }
biometricSettingsRepository.setIsUserInLockdown(false)
fakeUserRepository.setSelectedUserInfo(primaryUser)
biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true)
+ faceLockoutResetCallback.value.onLockoutReset(0)
bouncerRepository.setAlternateVisible(true)
keyguardRepository.setKeyguardShowing(true)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
index 264328b04227..def016ad8381 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt
@@ -26,7 +26,11 @@ import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,7 +53,6 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var authController: AuthController
@Captor
private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
@@ -68,7 +71,6 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
authController,
keyguardUpdateMonitor,
testScope.backgroundScope,
- dumpManager,
)
}
@@ -177,4 +179,129 @@ class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() {
callback.value.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT)
assertThat(availableFpSensorType()).isEqualTo(BiometricType.UNDER_DISPLAY_FINGERPRINT)
}
+
+ @Test
+ fun onFingerprintSuccess_successAuthenticationStatus() =
+ testScope.runTest {
+ val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+ updateMonitorCallback.value.onBiometricAuthenticated(
+ 0,
+ BiometricSourceType.FINGERPRINT,
+ true,
+ )
+
+ val status = authenticationStatus as SuccessFingerprintAuthenticationStatus
+ assertThat(status.userId).isEqualTo(0)
+ assertThat(status.isStrongBiometric).isEqualTo(true)
+ }
+
+ @Test
+ fun onFingerprintFailed_failedAuthenticationStatus() =
+ testScope.runTest {
+ val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+ updateMonitorCallback.value.onBiometricAuthFailed(
+ BiometricSourceType.FINGERPRINT,
+ )
+
+ assertThat(authenticationStatus)
+ .isInstanceOf(FailFingerprintAuthenticationStatus::class.java)
+ }
+
+ @Test
+ fun onFingerprintError_errorAuthenticationStatus() =
+ testScope.runTest {
+ val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+ updateMonitorCallback.value.onBiometricError(
+ 1,
+ "test_string",
+ BiometricSourceType.FINGERPRINT,
+ )
+
+ val status = authenticationStatus as ErrorFingerprintAuthenticationStatus
+ assertThat(status.msgId).isEqualTo(1)
+ assertThat(status.msg).isEqualTo("test_string")
+ }
+
+ @Test
+ fun onFingerprintHelp_helpAuthenticationStatus() =
+ testScope.runTest {
+ val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+ updateMonitorCallback.value.onBiometricHelp(
+ 1,
+ "test_string",
+ BiometricSourceType.FINGERPRINT,
+ )
+
+ val status = authenticationStatus as HelpFingerprintAuthenticationStatus
+ assertThat(status.msgId).isEqualTo(1)
+ assertThat(status.msg).isEqualTo("test_string")
+ }
+
+ @Test
+ fun onFingerprintAcquired_acquiredAuthenticationStatus() =
+ testScope.runTest {
+ val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+ updateMonitorCallback.value.onBiometricAcquired(
+ BiometricSourceType.FINGERPRINT,
+ 5,
+ )
+
+ val status = authenticationStatus as AcquiredFingerprintAuthenticationStatus
+ assertThat(status.acquiredInfo).isEqualTo(5)
+ }
+
+ @Test
+ fun onFaceCallbacks_fingerprintAuthenticationStatusIsUnchanged() =
+ testScope.runTest {
+ val authenticationStatus by collectLastValue(underTest.authenticationStatus)
+ runCurrent()
+
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
+ updateMonitorCallback.value.onBiometricAuthenticated(
+ 0,
+ BiometricSourceType.FACE,
+ true,
+ )
+ assertThat(authenticationStatus).isNull()
+
+ updateMonitorCallback.value.onBiometricAuthFailed(
+ BiometricSourceType.FACE,
+ )
+ assertThat(authenticationStatus).isNull()
+
+ updateMonitorCallback.value.onBiometricHelp(
+ 1,
+ "test_string",
+ BiometricSourceType.FACE,
+ )
+ assertThat(authenticationStatus).isNull()
+
+ updateMonitorCallback.value.onBiometricAcquired(
+ BiometricSourceType.FACE,
+ 5,
+ )
+ assertThat(authenticationStatus).isNull()
+
+ updateMonitorCallback.value.onBiometricError(
+ 1,
+ "test_string",
+ BiometricSourceType.FACE,
+ )
+ assertThat(authenticationStatus).isNull()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index f541815d2711..ba7d3490e56d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -31,11 +31,14 @@ import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
import com.android.systemui.dreams.DreamOverlayCallbackController
+import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.ScreenModel
+import com.android.systemui.keyguard.shared.model.ScreenState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -71,6 +74,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var statusBarStateController: StatusBarStateController
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+ @Mock private lateinit var screenLifecycle: ScreenLifecycle
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@Mock private lateinit var dozeTransitionListener: DozeTransitionListener
@Mock private lateinit var authController: AuthController
@@ -92,6 +96,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
KeyguardRepositoryImpl(
statusBarStateController,
wakefulnessLifecycle,
+ screenLifecycle,
biometricUnlockController,
keyguardStateController,
keyguardBypassController,
@@ -160,6 +165,16 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun dozeTimeTick() =
+ testScope.runTest {
+ var dozeTimeTickValue = collectLastValue(underTest.dozeTimeTick)
+ underTest.dozeTimeTick()
+ runCurrent()
+
+ assertThat(dozeTimeTickValue()).isNull()
+ }
+
+ @Test
fun isKeyguardShowing() =
testScope.runTest {
whenever(keyguardStateController.isShowing).thenReturn(false)
@@ -371,6 +386,48 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun screenModel() =
+ testScope.runTest {
+ val values = mutableListOf<ScreenModel>()
+ val job = underTest.screenModel.onEach(values::add).launchIn(this)
+
+ runCurrent()
+ val captor = argumentCaptor<ScreenLifecycle.Observer>()
+ verify(screenLifecycle).addObserver(captor.capture())
+
+ whenever(screenLifecycle.getScreenState()).thenReturn(ScreenLifecycle.SCREEN_TURNING_ON)
+ captor.value.onScreenTurningOn()
+ runCurrent()
+
+ whenever(screenLifecycle.getScreenState()).thenReturn(ScreenLifecycle.SCREEN_ON)
+ captor.value.onScreenTurnedOn()
+ runCurrent()
+
+ whenever(screenLifecycle.getScreenState())
+ .thenReturn(ScreenLifecycle.SCREEN_TURNING_OFF)
+ captor.value.onScreenTurningOff()
+ runCurrent()
+
+ whenever(screenLifecycle.getScreenState()).thenReturn(ScreenLifecycle.SCREEN_OFF)
+ captor.value.onScreenTurnedOff()
+ runCurrent()
+
+ assertThat(values.map { it.state })
+ .isEqualTo(
+ listOf(
+ // Initial value will be OFF
+ ScreenState.SCREEN_OFF,
+ ScreenState.SCREEN_TURNING_ON,
+ ScreenState.SCREEN_ON,
+ ScreenState.SCREEN_TURNING_OFF,
+ ScreenState.SCREEN_OFF,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
fun isUdfpsSupported() =
testScope.runTest {
whenever(keyguardUpdateMonitor.isUdfpsSupported).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index f9745779ecfb..ec30732dda23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -17,31 +17,43 @@
package com.android.systemui.keyguard.data.repository
import android.graphics.Point
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.core.animation.AnimatorTestRule
import androidx.test.filters.SmallTest
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.statusbar.CircleReveal
import com.android.systemui.statusbar.LightRevealEffect
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
@SmallTest
@RoboPilotTest
-@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
class LightRevealScrimRepositoryTest : SysuiTestCase() {
private lateinit var fakeKeyguardRepository: FakeKeyguardRepository
private lateinit var underTest: LightRevealScrimRepositoryImpl
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -50,112 +62,127 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() {
}
@Test
- fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() =
- runTest {
- val values = mutableListOf<LightRevealEffect>()
- val job = launch { underTest.revealEffect.collect { values.add(it) } }
-
- // We should initially emit the default reveal effect.
- runCurrent()
- values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
-
- // The source and sensor locations are still null, so we should still be using the
- // default reveal despite a biometric unlock.
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
-
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
+ fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest {
+ val values = mutableListOf<LightRevealEffect>()
+ val job = launch { underTest.revealEffect.collect { values.add(it) } }
+
+ fakeKeyguardRepository.setWakefulnessModel(
+ WakefulnessModel(
+ WakefulnessState.STARTING_TO_WAKE,
+ WakeSleepReason.OTHER,
+ WakeSleepReason.OTHER
)
+ )
+ // We should initially emit the default reveal effect.
+ runCurrent()
+ values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT })
- // We got a source but still have no sensor locations, so should be sticking with
- // the default effect.
- fakeKeyguardRepository.setBiometricUnlockSource(
- BiometricUnlockSource.FINGERPRINT_SENSOR
- )
+ // The source and sensor locations are still null, so we should still be using the
+ // default reveal despite a biometric unlock.
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- // We got a location for the face sensor, but we unlocked with fingerprint.
- val faceLocation = Point(250, 0)
- fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
+ // We got a source but still have no sensor locations, so should be sticking with
+ // the default effect.
+ fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
- val fingerprintLocation = Point(500, 500)
- fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
- fakeKeyguardRepository.setBiometricUnlockSource(
- BiometricUnlockSource.FINGERPRINT_SENSOR
- )
- fakeKeyguardRepository.setBiometricUnlockState(
- BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
- )
+ // We got a location for the face sensor, but we unlocked with fingerprint.
+ val faceLocation = Point(250, 0)
+ fakeKeyguardRepository.setFaceSensorLocation(faceLocation)
- // We should now have switched to the circle reveal, at the fingerprint location.
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
- val valuesPrevSize = values.size
- fakeKeyguardRepository.setBiometricUnlockState(
- BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
- )
- fakeKeyguardRepository.setBiometricUnlockState(
- BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
- )
- assertEquals(valuesPrevSize, values.size)
-
- // Non-biometric unlock, we should return to the default reveal.
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
-
- runCurrent()
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- { it == DEFAULT_REVEAL_EFFECT },
- )
+ // Now we have fingerprint sensor locations, and wake and unlock via fingerprint.
+ val fingerprintLocation = Point(500, 500)
+ fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation)
+ fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR)
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
+
+ // We should now have switched to the circle reveal, at the fingerprint location.
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ )
- // We already have a face location, so switching to face source should update the
- // CircleReveal.
- fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
- runCurrent()
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
- runCurrent()
-
- values.assertEffectsMatchPredicates(
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- { it == DEFAULT_REVEAL_EFFECT },
- {
- it is CircleReveal &&
- it.centerX == faceLocation.x &&
- it.centerY == faceLocation.y
- },
- )
+ // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals.
+ val valuesPrevSize = values.size
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING)
+ fakeKeyguardRepository.setBiometricUnlockState(
+ BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+ )
+ assertEquals(valuesPrevSize, values.size)
+
+ // Non-biometric unlock, we should return to the default reveal.
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
+
+ // We already have a face location, so switching to face source should update the
+ // CircleReveal.
+ fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR)
+ runCurrent()
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK)
+ runCurrent()
+
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ { it == DEFAULT_REVEAL_EFFECT },
+ { it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y },
+ )
+
+ job.cancel()
+ }
- job.cancel()
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ fun revealAmount_emitsTo1AfterAnimationStarted() =
+ runTest(UnconfinedTestDispatcher()) {
+ val value by collectLastValue(underTest.revealAmount)
+ underTest.startRevealAmountAnimator(true)
+ assertEquals(0.0f, value)
+ animatorTestRule.advanceTimeBy(500L)
+ assertEquals(1.0f, value)
+ }
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ fun revealAmount_emitsTo0AfterAnimationStartedReversed() =
+ runTest(UnconfinedTestDispatcher()) {
+ val value by collectLastValue(underTest.revealAmount)
+ underTest.startRevealAmountAnimator(false)
+ assertEquals(1.0f, value)
+ animatorTestRule.advanceTimeBy(500L)
+ assertEquals(0.0f, value)
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt
new file mode 100644
index 000000000000..3389fa9a48af
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BiometricMessageInteractorTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.util.IndicationHelper
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BiometricMessageInteractorTest : SysuiTestCase() {
+
+ private lateinit var underTest: BiometricMessageInteractor
+ private lateinit var testScope: TestScope
+ private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+ private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository
+
+ @Mock private lateinit var indicationHelper: IndicationHelper
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope()
+ fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+ fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+ underTest =
+ BiometricMessageInteractor(
+ mContext.resources,
+ fingerprintAuthRepository,
+ fingerprintPropertyRepository,
+ indicationHelper,
+ keyguardUpdateMonitor,
+ )
+ }
+
+ @Test
+ fun fingerprintErrorMessage() =
+ testScope.runTest {
+ val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage)
+
+ // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should NOT be suppressed
+ whenever(
+ indicationHelper.shouldSuppressErrorMsg(
+ FINGERPRINT,
+ FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE
+ )
+ )
+ .thenReturn(false)
+
+ // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE
+ fingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+ msg = "test"
+ )
+ )
+
+ // THEN fingerprintErrorMessage is updated
+ assertThat(fingerprintErrorMessage?.source).isEqualTo(FINGERPRINT)
+ assertThat(fingerprintErrorMessage?.type).isEqualTo(BiometricMessageType.ERROR)
+ assertThat(fingerprintErrorMessage?.id)
+ .isEqualTo(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE)
+ assertThat(fingerprintErrorMessage?.message).isEqualTo("test")
+ }
+
+ @Test
+ fun fingerprintErrorMessage_suppressedError() =
+ testScope.runTest {
+ val fingerprintErrorMessage by collectLastValue(underTest.fingerprintErrorMessage)
+
+ // GIVEN FINGERPRINT_ERROR_HW_UNAVAILABLE should be suppressed
+ whenever(
+ indicationHelper.shouldSuppressErrorMsg(
+ FINGERPRINT,
+ FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE
+ )
+ )
+ .thenReturn(true)
+
+ // WHEN authentication status error is FINGERPRINT_ERROR_HW_UNAVAILABLE
+ fingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ msgId = FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE,
+ msg = "test"
+ )
+ )
+
+ // THEN fingerprintErrorMessage isn't update - it's still null
+ assertThat(fingerprintErrorMessage).isNull()
+ }
+
+ @Test
+ fun fingerprintHelpMessage() =
+ testScope.runTest {
+ val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage)
+
+ // GIVEN primary auth is NOT required
+ whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(true)
+
+ // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY
+ fingerprintAuthRepository.setAuthenticationStatus(
+ HelpFingerprintAuthenticationStatus(
+ msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
+ msg = "test"
+ )
+ )
+
+ // THEN fingerprintHelpMessage is updated
+ assertThat(fingerprintHelpMessage?.source).isEqualTo(FINGERPRINT)
+ assertThat(fingerprintHelpMessage?.type).isEqualTo(BiometricMessageType.HELP)
+ assertThat(fingerprintHelpMessage?.id)
+ .isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY)
+ assertThat(fingerprintHelpMessage?.message).isEqualTo("test")
+ }
+
+ @Test
+ fun fingerprintHelpMessage_primaryAuthRequired() =
+ testScope.runTest {
+ val fingerprintHelpMessage by collectLastValue(underTest.fingerprintHelpMessage)
+
+ // GIVEN primary auth is required
+ whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(false)
+
+ // WHEN authentication status help is FINGERPRINT_ACQUIRED_IMAGER_DIRTY
+ fingerprintAuthRepository.setAuthenticationStatus(
+ HelpFingerprintAuthenticationStatus(
+ msgId = FingerprintManager.FINGERPRINT_ACQUIRED_IMAGER_DIRTY,
+ msg = "test"
+ )
+ )
+
+ // THEN fingerprintHelpMessage isn't update - it's still null
+ assertThat(fingerprintHelpMessage).isNull()
+ }
+
+ @Test
+ fun fingerprintFailMessage_nonUdfps() =
+ testScope.runTest {
+ val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage)
+
+ // GIVEN primary auth is NOT required
+ whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(true)
+
+ // GIVEN rear fingerprint (not UDFPS)
+ fingerprintPropertyRepository.setProperties(
+ 0,
+ SensorStrength.STRONG,
+ FingerprintSensorType.REAR,
+ mapOf()
+ )
+
+ // WHEN authentication status fail
+ fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+ // THEN fingerprintFailMessage is updated
+ assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT)
+ assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL)
+ assertThat(fingerprintFailMessage?.id)
+ .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED)
+ assertThat(fingerprintFailMessage?.message)
+ .isEqualTo(
+ mContext.resources.getString(
+ com.android.internal.R.string.fingerprint_error_not_match
+ )
+ )
+ }
+
+ @Test
+ fun fingerprintFailMessage_udfps() =
+ testScope.runTest {
+ val fingerprintFailMessage by collectLastValue(underTest.fingerprintFailMessage)
+
+ // GIVEN primary auth is NOT required
+ whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(true)
+
+ // GIVEN UDFPS
+ fingerprintPropertyRepository.setProperties(
+ 0,
+ SensorStrength.STRONG,
+ FingerprintSensorType.UDFPS_OPTICAL,
+ mapOf()
+ )
+
+ // WHEN authentication status fail
+ fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+ // THEN fingerprintFailMessage is updated to udfps message
+ assertThat(fingerprintFailMessage?.source).isEqualTo(FINGERPRINT)
+ assertThat(fingerprintFailMessage?.type).isEqualTo(BiometricMessageType.FAIL)
+ assertThat(fingerprintFailMessage?.id)
+ .isEqualTo(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED)
+ assertThat(fingerprintFailMessage?.message)
+ .isEqualTo(
+ mContext.resources.getString(
+ com.android.internal.R.string.fingerprint_udfps_error_not_match
+ )
+ )
+ }
+
+ @Test
+ fun fingerprintFailedMessage_primaryAuthRequired() =
+ testScope.runTest {
+ val fingerprintFailedMessage by collectLastValue(underTest.fingerprintFailMessage)
+
+ // GIVEN primary auth is required
+ whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(false)
+
+ // WHEN authentication status fail
+ fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+ // THEN fingerprintFailedMessage isn't update - it's still null
+ assertThat(fingerprintFailedMessage).isNull()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
index 3e81cd336824..8636dd8df3b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt
@@ -41,7 +41,7 @@ import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepo
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.shared.model.ErrorAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -74,6 +74,8 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository
+ private lateinit var fakeDeviceEntryFingerprintAuthRepository:
+ FakeDeviceEntryFingerprintAuthRepository
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@@ -95,6 +97,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
)
.keyguardTransitionInteractor
+ fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
underTest =
SystemUIKeyguardFaceAuthInteractor(
mContext,
@@ -121,13 +124,14 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
mock(KeyguardStateController::class.java),
bouncerRepository,
mock(BiometricSettingsRepository::class.java),
- FakeDeviceEntryFingerprintAuthRepository(),
FakeSystemClock(),
+ keyguardUpdateMonitor,
),
keyguardTransitionInteractor,
featureFlags,
FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
keyguardUpdateMonitor,
+ fakeDeviceEntryFingerprintAuthRepository
)
}
@@ -160,7 +164,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
underTest.onDeviceLifted()
- val outputValue = authenticationStatus()!! as ErrorAuthenticationStatus
+ val outputValue = authenticationStatus()!! as ErrorFaceAuthenticationStatus
assertThat(outputValue.msgId)
.isEqualTo(BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT)
assertThat(outputValue.msg).isEqualTo("Face Unlock unavailable")
@@ -336,4 +340,15 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() {
assertThat(faceAuthRepository.runningAuthRequest.value)
.isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false))
}
+
+ @Test
+ fun faceUnlockIsDisabledWhenFpIsLockedOut() =
+ testScope.runTest {
+ underTest.start()
+
+ fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(faceAuthRepository.wasDisabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index 6e7ba6dd1ecb..906d94859140 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -27,27 +27,37 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.Spy
@SmallTest
@RoboPilotTest
+@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class LightRevealScrimInteractorTest : SysuiTestCase() {
private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
- private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+
+ @Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
+
+ private val testScope = TestScope()
private val keyguardTransitionInteractor =
KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
+ scope = testScope.backgroundScope,
repository = fakeKeyguardTransitionRepository,
)
.keyguardTransitionInteractor
@@ -69,9 +79,9 @@ class LightRevealScrimInteractorTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
underTest =
LightRevealScrimInteractor(
- fakeKeyguardTransitionRepository,
keyguardTransitionInteractor,
- fakeLightRevealScrimRepository
+ fakeLightRevealScrimRepository,
+ testScope.backgroundScope
)
}
@@ -110,52 +120,36 @@ class LightRevealScrimInteractorTest : SysuiTestCase() {
}
@Test
- fun revealAmount_invertedWhenAppropriate() =
- runTest(UnconfinedTestDispatcher()) {
- val values = mutableListOf<Float>()
- val job = underTest.revealAmount.onEach(values::add).launchIn(this)
-
+ fun lightRevealEffect_startsAnimationOnlyForDifferentStateTargets() =
+ testScope.runTest {
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- value = 0.3f
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.OFF,
+ to = KeyguardState.OFF
)
)
-
- assertEquals(values, listOf(0.3f))
+ runCurrent()
+ verify(fakeLightRevealScrimRepository, never()).startRevealAmountAnimator(anyBoolean())
fakeKeyguardTransitionRepository.sendTransitionStep(
TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.AOD,
- value = 0.3f
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN
)
)
-
- assertEquals(values, listOf(0.3f, 0.7f))
-
- job.cancel()
- }
-
- @Test
- fun revealAmount_ignoresTransitionsThatDoNotAffectRevealAmount() =
- runTest(UnconfinedTestDispatcher()) {
- val values = mutableListOf<Float>()
- val job = underTest.revealAmount.onEach(values::add).launchIn(this)
-
- fakeKeyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.AOD, value = 0.3f)
- )
-
- assertEquals(values, emptyList<Float>())
+ runCurrent()
+ verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(true)
fakeKeyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f)
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING
+ )
)
-
- assertEquals(values, emptyList<Float>())
-
- job.cancel()
+ runCurrent()
+ verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
new file mode 100644
index 000000000000..6e52d1af7eb1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.hardware.biometrics.BiometricSourceType
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.util.IndicationHelper
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.ActivityStarter.OnDismissAction
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() {
+
+ private lateinit var underTest: OccludingAppDeviceEntryInteractor
+ private lateinit var testScope: TestScope
+ private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
+ private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+ private lateinit var fingerprintAuthRepository: FakeDeviceEntryFingerprintAuthRepository
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+ private lateinit var configurationRepository: FakeConfigurationRepository
+ private lateinit var featureFlags: FakeFeatureFlags
+ private lateinit var trustRepository: FakeTrustRepository
+
+ @Mock private lateinit var indicationHelper: IndicationHelper
+ @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var mockedContext: Context
+ @Mock private lateinit var activityStarter: ActivityStarter
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope()
+ biometricSettingsRepository = FakeBiometricSettingsRepository()
+ fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
+ fingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
+ keyguardRepository = FakeKeyguardRepository()
+ bouncerRepository = FakeKeyguardBouncerRepository()
+ configurationRepository = FakeConfigurationRepository()
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.FACE_AUTH_REFACTOR, false)
+ set(Flags.DELAY_BOUNCER, false)
+ }
+ trustRepository = FakeTrustRepository()
+ underTest =
+ OccludingAppDeviceEntryInteractor(
+ BiometricMessageInteractor(
+ mContext.resources,
+ fingerprintAuthRepository,
+ fingerprintPropertyRepository,
+ indicationHelper,
+ keyguardUpdateMonitor,
+ ),
+ fingerprintAuthRepository,
+ KeyguardInteractor(
+ keyguardRepository,
+ commandQueue = mock(),
+ featureFlags,
+ bouncerRepository,
+ configurationRepository,
+ ),
+ PrimaryBouncerInteractor(
+ bouncerRepository,
+ primaryBouncerView = mock(),
+ mainHandler = mock(),
+ keyguardStateController = mock(),
+ keyguardSecurityModel = mock(),
+ primaryBouncerCallbackInteractor = mock(),
+ falsingCollector = mock(),
+ dismissCallbackRegistry = mock(),
+ context,
+ keyguardUpdateMonitor,
+ trustRepository,
+ featureFlags,
+ testScope.backgroundScope,
+ ),
+ AlternateBouncerInteractor(
+ statusBarStateController = mock(),
+ keyguardStateController = mock(),
+ bouncerRepository,
+ biometricSettingsRepository,
+ FakeSystemClock(),
+ keyguardUpdateMonitor,
+ ),
+ testScope.backgroundScope,
+ mockedContext,
+ activityStarter,
+ )
+ }
+
+ @Test
+ fun fingerprintSuccess_goToHomeScreen() =
+ testScope.runTest {
+ givenOnOccludingApp(true)
+ fingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ verifyGoToHomeScreen()
+ }
+
+ @Test
+ fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() =
+ testScope.runTest {
+ givenOnOccludingApp(false)
+ fingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ verifyNeverGoToHomeScreen()
+ }
+
+ @Test
+ fun lockout_goToHomeScreenOnDismissAction() =
+ testScope.runTest {
+ givenOnOccludingApp(true)
+ fingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+ "lockoutTest"
+ )
+ )
+ runCurrent()
+ verifyGoToHomeScreenOnDismiss()
+ }
+
+ @Test
+ fun lockout_notOnOccludingApp_neverGoToHomeScreen() =
+ testScope.runTest {
+ givenOnOccludingApp(false)
+ fingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+ "lockoutTest"
+ )
+ )
+ runCurrent()
+ verifyNeverGoToHomeScreen()
+ }
+
+ @Test
+ fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+
+ givenOnOccludingApp(true)
+ givenPrimaryAuthRequired(false)
+ runCurrent()
+ // WHEN a fp failure come in
+ fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+ // THEN message set to failure
+ assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL)
+
+ // GIVEN fingerprint shouldn't run
+ givenOnOccludingApp(false)
+ runCurrent()
+ // WHEN another fp failure arrives
+ fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+
+ // THEN message set to null
+ assertThat(message).isNull()
+ }
+
+ @Test
+ fun message_fpErrorHelpFailOnOccludingApp() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+
+ givenOnOccludingApp(true)
+ givenPrimaryAuthRequired(false)
+ runCurrent()
+
+ // ERROR message
+ fingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+ "testError",
+ )
+ )
+ assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
+ assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT)
+ assertThat(message?.message).isEqualTo("testError")
+ assertThat(message?.type).isEqualTo(BiometricMessageType.ERROR)
+
+ // HELP message
+ fingerprintAuthRepository.setAuthenticationStatus(
+ HelpFingerprintAuthenticationStatus(
+ FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL,
+ "testHelp",
+ )
+ )
+ assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
+ assertThat(message?.id).isEqualTo(FingerprintManager.FINGERPRINT_ACQUIRED_PARTIAL)
+ assertThat(message?.message).isEqualTo("testHelp")
+ assertThat(message?.type).isEqualTo(BiometricMessageType.HELP)
+
+ // FAIL message
+ fingerprintAuthRepository.setAuthenticationStatus(FailFingerprintAuthenticationStatus)
+ assertThat(message?.source).isEqualTo(BiometricSourceType.FINGERPRINT)
+ assertThat(message?.id)
+ .isEqualTo(KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED)
+ assertThat(message?.type).isEqualTo(BiometricMessageType.FAIL)
+ }
+
+ private fun givenOnOccludingApp(isOnOccludingApp: Boolean) {
+ keyguardRepository.setKeyguardOccluded(isOnOccludingApp)
+ keyguardRepository.setKeyguardShowing(isOnOccludingApp)
+ bouncerRepository.setPrimaryShow(!isOnOccludingApp)
+ bouncerRepository.setAlternateVisible(!isOnOccludingApp)
+ }
+
+ private fun givenPrimaryAuthRequired(required: Boolean) {
+ whenever(keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean()))
+ .thenReturn(!required)
+ }
+
+ private fun verifyGoToHomeScreen() {
+ val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(mockedContext).startActivity(intentCaptor.capture())
+
+ assertThat(intentCaptor.value.hasCategory(Intent.CATEGORY_HOME)).isTrue()
+ assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_MAIN)
+ }
+
+ private fun verifyNeverGoToHomeScreen() {
+ verify(mockedContext, never()).startActivity(any())
+ verify(activityStarter, never())
+ .dismissKeyguardThenExecute(any(OnDismissAction::class.java), isNull(), eq(false))
+ }
+
+ private fun verifyGoToHomeScreenOnDismiss() {
+ val onDimissActionCaptor = ArgumentCaptor.forClass(OnDismissAction::class.java)
+ verify(activityStarter)
+ .dismissKeyguardThenExecute(onDimissActionCaptor.capture(), isNull(), eq(false))
+ onDimissActionCaptor.value.onDismiss()
+
+ verifyGoToHomeScreen()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
index 1baca2184e9b..b019a21387da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
@@ -33,6 +33,9 @@ import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
@@ -46,6 +49,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@ExperimentalCoroutinesApi
@@ -63,19 +67,21 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() {
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
private lateinit var burnInInteractor: BurnInInteractor
+ private lateinit var shadeRepository: FakeShadeRepository
@Mock private lateinit var burnInHelper: BurnInHelperWrapper
+ @Mock private lateinit var dialogManager: SystemUIDialogManager
private lateinit var underTest: UdfpsKeyguardInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
-
testScope = TestScope()
configRepository = FakeConfigurationRepository()
keyguardRepository = FakeKeyguardRepository()
bouncerRepository = FakeKeyguardBouncerRepository()
+ shadeRepository = FakeShadeRepository()
fakeCommandQueue = FakeCommandQueue()
featureFlags =
FakeFeatureFlags().apply {
@@ -102,6 +108,8 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() {
bouncerRepository,
configRepository,
),
+ shadeRepository,
+ dialogManager,
)
}
@@ -142,6 +150,61 @@ class UdfpsKeyguardInteractorTest : SysuiTestCase() {
assertThat(burnInOffsets?.burnInXOffset).isEqualTo(burnInXOffset)
}
+ @Test
+ fun dialogHideAffordances() =
+ testScope.runTest {
+ val dialogHideAffordancesRequest by
+ collectLastValue(underTest.dialogHideAffordancesRequest)
+ runCurrent()
+ val captor = argumentCaptor<SystemUIDialogManager.Listener>()
+ verify(dialogManager).registerListener(captor.capture())
+
+ captor.value.shouldHideAffordances(false)
+ assertThat(dialogHideAffordancesRequest).isEqualTo(false)
+
+ captor.value.shouldHideAffordances(true)
+ assertThat(dialogHideAffordancesRequest).isEqualTo(true)
+
+ captor.value.shouldHideAffordances(false)
+ assertThat(dialogHideAffordancesRequest).isEqualTo(false)
+ }
+
+ @Test
+ fun shadeExpansion_updates() =
+ testScope.runTest {
+ keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ val shadeExpansion by collectLastValue(underTest.shadeExpansion)
+ assertThat(shadeExpansion).isEqualTo(0f)
+
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(.5f)
+ assertThat(shadeExpansion).isEqualTo(.5f)
+
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(.7f)
+ assertThat(shadeExpansion).isEqualTo(.7f)
+
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(.22f)
+ assertThat(shadeExpansion).isEqualTo(.22f)
+
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+ assertThat(shadeExpansion).isEqualTo(1f)
+ }
+
+ @Test
+ fun qsProgress_updates() =
+ testScope.runTest {
+ val qsProgress by collectLastValue(underTest.qsProgress)
+ assertThat(qsProgress).isEqualTo(0f)
+
+ shadeRepository.setQsExpansion(.22f)
+ assertThat(qsProgress).isEqualTo(.44f)
+
+ shadeRepository.setQsExpansion(.5f)
+ assertThat(qsProgress).isEqualTo(1f)
+
+ shadeRepository.setQsExpansion(.7f)
+ assertThat(qsProgress).isEqualTo(1f)
+ }
+
private fun initializeBurnInOffsets() {
whenever(burnInHelper.burnInProgressOffset()).thenReturn(burnInProgress)
whenever(burnInHelper.burnInOffset(anyInt(), /* xAxis */ eq(true)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
index 436c09ca4a05..b985b3ca83da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsAodViewModelTest.kt
@@ -32,6 +32,8 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -58,7 +60,9 @@ class UdfpsAodViewModelTest : SysuiTestCase() {
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
+ private lateinit var shadeRepository: FakeShadeRepository
+ @Mock private lateinit var dialogManager: SystemUIDialogManager
@Mock private lateinit var burnInHelper: BurnInHelperWrapper
@Before
@@ -70,6 +74,7 @@ class UdfpsAodViewModelTest : SysuiTestCase() {
keyguardRepository = FakeKeyguardRepository()
bouncerRepository = FakeKeyguardBouncerRepository()
fakeCommandQueue = FakeCommandQueue()
+ shadeRepository = FakeShadeRepository()
featureFlags =
FakeFeatureFlags().apply {
set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
@@ -93,6 +98,8 @@ class UdfpsAodViewModelTest : SysuiTestCase() {
bouncerRepository,
configRepository,
),
+ shadeRepository,
+ dialogManager,
)
underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
index a30e2a601e9d..0fbcec23f247 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsFingerprintViewModelTest.kt
@@ -33,6 +33,8 @@ import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -60,8 +62,10 @@ class UdfpsFingerprintViewModelTest : SysuiTestCase() {
private lateinit var fakeCommandQueue: FakeCommandQueue
private lateinit var featureFlags: FakeFeatureFlags
private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+ private lateinit var shadeRepository: FakeShadeRepository
@Mock private lateinit var burnInHelper: BurnInHelperWrapper
+ @Mock private lateinit var dialogManager: SystemUIDialogManager
@Before
fun setUp() {
@@ -79,35 +83,39 @@ class UdfpsFingerprintViewModelTest : SysuiTestCase() {
}
bouncerRepository = FakeKeyguardBouncerRepository()
transitionRepository = FakeKeyguardTransitionRepository()
+ shadeRepository = FakeShadeRepository()
val transitionInteractor =
KeyguardTransitionInteractor(
transitionRepository,
testScope.backgroundScope,
)
- val udfpsKeyguardInteractor =
- UdfpsKeyguardInteractor(
+ val keyguardInteractor =
+ KeyguardInteractor(
+ keyguardRepository,
+ fakeCommandQueue,
+ featureFlags,
+ bouncerRepository,
configRepository,
- BurnInInteractor(
- context,
- burnInHelper,
- testScope.backgroundScope,
- configRepository,
- FakeSystemClock(),
- ),
- KeyguardInteractor(
- keyguardRepository,
- fakeCommandQueue,
- featureFlags,
- bouncerRepository,
- configRepository,
- ),
)
underTest =
FingerprintViewModel(
context,
transitionInteractor,
- udfpsKeyguardInteractor,
+ UdfpsKeyguardInteractor(
+ configRepository,
+ BurnInInteractor(
+ context,
+ burnInHelper,
+ testScope.backgroundScope,
+ configRepository,
+ FakeSystemClock(),
+ ),
+ keyguardInteractor,
+ shadeRepository,
+ dialogManager,
+ ),
+ keyguardInteractor,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
index d58ceee40c68..41ae93183850 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
@@ -20,12 +20,27 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.Utils
import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.UdfpsKeyguardInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.wm.shell.animation.Interpolators
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -35,6 +50,8 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.MockitoAnnotations
/** Tests UDFPS lockscreen view model transitions. */
@@ -48,26 +65,63 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
private val alternateBouncerColor =
Utils.getColorAttrDefaultColor(context, alternateBouncerResId)
+ @Mock private lateinit var dialogManager: SystemUIDialogManager
+
private lateinit var underTest: UdfpsLockscreenViewModel
private lateinit var testScope: TestScope
private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+ private lateinit var configRepository: FakeConfigurationRepository
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
+ private lateinit var shadeRepository: FakeShadeRepository
+ private lateinit var featureFlags: FakeFeatureFlags
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
testScope = TestScope()
transitionRepository = FakeKeyguardTransitionRepository()
- val transitionInteractor =
- KeyguardTransitionInteractor(
- transitionRepository,
- testScope.backgroundScope,
+ configRepository = FakeConfigurationRepository()
+ keyguardRepository = FakeKeyguardRepository()
+ bouncerRepository = FakeKeyguardBouncerRepository()
+ shadeRepository = FakeShadeRepository()
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS, true)
+ set(Flags.FACE_AUTH_REFACTOR, false)
+ }
+ val keyguardInteractor =
+ KeyguardInteractor(
+ keyguardRepository,
+ commandQueue = mock(),
+ featureFlags,
+ bouncerRepository,
+ configRepository,
)
+
underTest =
UdfpsLockscreenViewModel(
context,
lockscreenColorResId,
alternateBouncerResId,
- transitionInteractor,
+ KeyguardTransitionInteractor(
+ transitionRepository,
+ testScope.backgroundScope,
+ ),
+ UdfpsKeyguardInteractor(
+ configRepository,
+ BurnInInteractor(
+ context,
+ burnInHelperWrapper = mock(),
+ testScope.backgroundScope,
+ configRepository,
+ FakeSystemClock(),
+ ),
+ keyguardInteractor,
+ shadeRepository,
+ dialogManager,
+ ),
+ keyguardInteractor,
)
}
@@ -125,6 +179,7 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
testScope.runTest {
val transition by collectLastValue(underTest.transition)
val visible by collectLastValue(underTest.visible)
+ keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
// TransitionState.STARTED: lockscreen -> AOD
transitionRepository.sendTransitionStep(
@@ -176,6 +231,56 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
}
@Test
+ fun lockscreenShadeLockedToAod() =
+ testScope.runTest {
+ val transition by collectLastValue(underTest.transition)
+ val visible by collectLastValue(underTest.visible)
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+
+ // TransitionState.STARTED: lockscreen -> AOD
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 0f,
+ transitionState = TransitionState.STARTED,
+ ownerName = "lockscreenToAod",
+ )
+ )
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(0f)
+ assertThat(visible).isFalse()
+
+ // TransitionState.RUNNING: lockscreen -> AOD
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = .6f,
+ transitionState = TransitionState.RUNNING,
+ ownerName = "lockscreenToAod",
+ )
+ )
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(0f)
+ assertThat(visible).isFalse()
+
+ // TransitionState.FINISHED: lockscreen -> AOD
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 1f,
+ transitionState = TransitionState.FINISHED,
+ ownerName = "lockscreenToAod",
+ )
+ )
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(0f)
+ assertThat(visible).isFalse()
+ }
+
+ @Test
fun aodToLockscreen() =
testScope.runTest {
val transition by collectLastValue(underTest.transition)
@@ -235,6 +340,7 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
testScope.runTest {
val transition by collectLastValue(underTest.transition)
val visible by collectLastValue(underTest.visible)
+ keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
// TransitionState.STARTED: lockscreen -> alternate bouncer
transitionRepository.sendTransitionStep(
@@ -398,6 +504,7 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
testScope.runTest {
val transition by collectLastValue(underTest.transition)
val visible by collectLastValue(underTest.visible)
+ keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
// TransitionState.STARTED: lockscreen -> occluded
transitionRepository.sendTransitionStep(
@@ -502,4 +609,152 @@ class UdfpsLockscreenViewModelTest : SysuiTestCase() {
assertThat(transition?.color).isEqualTo(lockscreenColor)
assertThat(visible).isTrue()
}
+
+ @Test
+ fun qsProgressChange() =
+ testScope.runTest {
+ val transition by collectLastValue(underTest.transition)
+ val visible by collectLastValue(underTest.visible)
+ givenTransitionToLockscreenFinished()
+
+ // qsExpansion = 0f
+ shadeRepository.setQsExpansion(0f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(1f)
+ assertThat(visible).isEqualTo(true)
+
+ // qsExpansion = .25
+ shadeRepository.setQsExpansion(.2f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(.6f)
+ assertThat(visible).isEqualTo(true)
+
+ // qsExpansion = .5
+ shadeRepository.setQsExpansion(.5f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(0f)
+ assertThat(visible).isEqualTo(false)
+
+ // qsExpansion = 1
+ shadeRepository.setQsExpansion(1f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(0f)
+ assertThat(visible).isEqualTo(false)
+ }
+
+ @Test
+ fun shadeExpansionChanged() =
+ testScope.runTest {
+ val transition by collectLastValue(underTest.transition)
+ val visible by collectLastValue(underTest.visible)
+ givenTransitionToLockscreenFinished()
+
+ // shadeExpansion = 0f
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(0f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(1f)
+ assertThat(visible).isEqualTo(true)
+
+ // shadeExpansion = .2
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(.2f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(.8f)
+ assertThat(visible).isEqualTo(true)
+
+ // shadeExpansion = .5
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(.5f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(.5f)
+ assertThat(visible).isEqualTo(true)
+
+ // shadeExpansion = 1
+ shadeRepository.setUdfpsTransitionToFullShadeProgress(1f)
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(0f)
+ assertThat(visible).isEqualTo(false)
+ }
+
+ @Test
+ fun dialogHideAffordancesRequestChanged() =
+ testScope.runTest {
+ val transition by collectLastValue(underTest.transition)
+ givenTransitionToLockscreenFinished()
+ runCurrent()
+ val captor = argumentCaptor<SystemUIDialogManager.Listener>()
+ Mockito.verify(dialogManager).registerListener(captor.capture())
+
+ captor.value.shouldHideAffordances(true)
+ assertThat(transition?.alpha).isEqualTo(0f)
+
+ captor.value.shouldHideAffordances(false)
+ assertThat(transition?.alpha).isEqualTo(1f)
+ }
+
+ @Test
+ fun occludedToAlternateBouncer() =
+ testScope.runTest {
+ val transition by collectLastValue(underTest.transition)
+ val visible by collectLastValue(underTest.visible)
+
+ // TransitionState.STARTED: occluded -> alternate bouncer
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ value = 0f,
+ transitionState = TransitionState.STARTED,
+ ownerName = "occludedToAlternateBouncer",
+ )
+ )
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(1f)
+ assertThat(transition?.scale).isEqualTo(0f)
+ assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+ assertThat(visible).isTrue()
+
+ // TransitionState.RUNNING: occluded -> alternate bouncer
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ value = .6f,
+ transitionState = TransitionState.RUNNING,
+ ownerName = "occludedToAlternateBouncer",
+ )
+ )
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(1f)
+ assertThat(transition?.scale)
+ .isEqualTo(Interpolators.FAST_OUT_SLOW_IN.getInterpolation(.6f))
+ assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+ assertThat(visible).isTrue()
+
+ // TransitionState.FINISHED: occluded -> alternate bouncer
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ value = 1f,
+ transitionState = TransitionState.FINISHED,
+ ownerName = "occludedToAlternateBouncer",
+ )
+ )
+ runCurrent()
+ assertThat(transition?.alpha).isEqualTo(1f)
+ assertThat(transition?.scale).isEqualTo(1f)
+ assertThat(transition?.color).isEqualTo(alternateBouncerColor)
+ assertThat(visible).isTrue()
+ }
+
+ private suspend fun givenTransitionToLockscreenFinished() {
+ transitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ value = 1f,
+ transitionState = TransitionState.FINISHED,
+ ownerName = "givenTransitionToLockscreenFinished",
+ )
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt b/packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt
new file mode 100644
index 000000000000..272d686e974b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/core/FakeLogBuffer.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.core
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogMessageImpl
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import org.mockito.Mockito.anyString
+
+/**
+ * A fake [LogBuffer] used for testing that obtains a real [LogMessage] to prevent a
+ * [NullPointerException].
+ */
+class FakeLogBuffer private constructor() {
+ class Factory private constructor() {
+ companion object {
+ fun create(): LogBuffer {
+ val logBuffer = mock<LogBuffer>()
+ whenever(
+ logBuffer.obtain(
+ tag = anyString(),
+ level = any(),
+ messagePrinter = any(),
+ exception = nullable(),
+ )
+ )
+ .thenReturn(LogMessageImpl.Factory.create())
+ return logBuffer
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
index b4b307301138..9a90a5ceb259 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt
@@ -216,9 +216,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var recCardTitle: TextView
@Mock private lateinit var coverItem: ImageView
@Mock private lateinit var matrix: Matrix
- private lateinit var coverItem1: ImageView
- private lateinit var coverItem2: ImageView
- private lateinit var coverItem3: ImageView
private lateinit var recTitle1: TextView
private lateinit var recTitle2: TextView
private lateinit var recTitle3: TextView
@@ -233,7 +230,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
FakeFeatureFlags().apply {
this.set(Flags.UMO_SURFACE_RIPPLE, false)
this.set(Flags.UMO_TURBULENCE_NOISE, false)
- this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false)
}
@Mock private lateinit var globalSettings: GlobalSettings
@@ -467,21 +463,25 @@ public class MediaControlPanelTest : SysuiTestCase() {
recSubtitle3 = TextView(context)
whenever(recommendationViewHolder.recommendations).thenReturn(view)
- whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon)
-
- // Add a recommendation item
- coverItem1 = ImageView(context).also { it.setId(R.id.media_cover1) }
- coverItem2 = ImageView(context).also { it.setId(R.id.media_cover2) }
- coverItem3 = ImageView(context).also { it.setId(R.id.media_cover3) }
-
+ whenever(recommendationViewHolder.mediaAppIcons)
+ .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
+ whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
whenever(recommendationViewHolder.mediaCoverItems)
- .thenReturn(listOf(coverItem1, coverItem2, coverItem3))
+ .thenReturn(listOf(coverItem, coverItem, coverItem))
whenever(recommendationViewHolder.mediaCoverContainers)
.thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
whenever(recommendationViewHolder.mediaTitles)
.thenReturn(listOf(recTitle1, recTitle2, recTitle3))
whenever(recommendationViewHolder.mediaSubtitles)
.thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
+ whenever(recommendationViewHolder.mediaProgressBars)
+ .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
+ whenever(coverItem.imageMatrix).thenReturn(matrix)
+
+ // set ids for recommendation containers
+ whenever(coverContainer1.id).thenReturn(1)
+ whenever(coverContainer2.id).thenReturn(2)
+ whenever(coverContainer3.id).thenReturn(3)
whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder)
@@ -1561,7 +1561,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
val description = descriptionCaptor.value.toString()
- assertThat(description).contains(REC_APP_NAME)
+ assertThat(description)
+ .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header))
}
@Test
@@ -1585,7 +1586,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(viewHolder.player).contentDescription = descriptionCaptor.capture()
val description = descriptionCaptor.value.toString()
- assertThat(description).contains(REC_APP_NAME)
+ assertThat(description)
+ .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header))
}
@Test
@@ -2151,7 +2153,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindRecommendation_setAfterExecutors() {
- setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val data =
smartspaceData.copy(
@@ -2189,7 +2190,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindRecommendationWithProgressBars() {
useRealConstraintSets()
- setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val bundle =
Bundle().apply {
@@ -2236,7 +2236,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() {
useRealConstraintSets()
- setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val data =
smartspaceData.copy(
@@ -2290,7 +2289,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Test
fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() {
useRealConstraintSets()
- setupUpdatedRecommendationViewHolder()
val albumArt = getColorIcon(Color.RED)
val data =
smartspaceData.copy(
@@ -2505,27 +2503,6 @@ public class MediaControlPanelTest : SysuiTestCase() {
verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent))
}
- private fun setupUpdatedRecommendationViewHolder() {
- fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true)
- whenever(recommendationViewHolder.mediaAppIcons)
- .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem))
- whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle)
- whenever(recommendationViewHolder.mediaCoverContainers)
- .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3))
- whenever(recommendationViewHolder.mediaCoverItems)
- .thenReturn(listOf(coverItem, coverItem, coverItem))
- whenever(recommendationViewHolder.mediaProgressBars)
- .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3))
- whenever(recommendationViewHolder.mediaSubtitles)
- .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3))
- whenever(coverItem.imageMatrix).thenReturn(matrix)
-
- // set ids for recommendation containers
- whenever(coverContainer1.id).thenReturn(1)
- whenever(coverContainer2.id).thenReturn(2)
- whenever(coverContainer3.id).thenReturn(3)
- }
-
private fun getColorIcon(color: Int): Icon {
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
index c9956f36dbeb..ba97df910e43 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt
@@ -201,8 +201,8 @@ class MediaViewControllerTest : SysuiTestCase() {
whenever(mockCopiedState.widgetStates)
.thenReturn(
mutableMapOf(
- R.id.media_title1 to mediaTitleWidgetState,
- R.id.media_subtitle1 to mediaSubTitleWidgetState,
+ R.id.media_title to mediaTitleWidgetState,
+ R.id.media_subtitle to mediaSubTitleWidgetState,
R.id.media_cover1_container to mediaContainerWidgetState
)
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index f79c53d10b52..ab24c46825e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -63,7 +63,6 @@ import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -124,7 +123,7 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
mMediaOutputController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
index f8971fd7c99c..45e8e270c3de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
@@ -70,7 +70,6 @@ import org.junit.runner.RunWith;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -126,7 +125,7 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 9f06b5fcc903..a59ea20ead26 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -93,7 +93,6 @@ import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -197,7 +196,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false);
@@ -279,7 +278,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mSpyContext, null,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
mMediaOutputController.start(mCb);
@@ -309,7 +308,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mSpyContext, null,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
mMediaOutputController.start(mCb);
@@ -530,7 +529,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
"",
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
testMediaOutputController.start(mCb);
reset(mCb);
@@ -553,7 +552,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
"",
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
testMediaOutputController.start(mCb);
reset(mCb);
@@ -589,7 +588,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
null,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
@@ -606,7 +605,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
null,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
@@ -888,7 +887,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mSpyContext, null,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
assertThat(mMediaOutputController.getNotificationIcon()).isNull();
@@ -1080,7 +1079,7 @@ public class MediaOutputControllerTest extends SysuiTestCase {
null,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
testMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index a14ff2f72f1f..3e69a29bd963 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -67,7 +67,6 @@ import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
-import java.util.Optional;
import java.util.function.Consumer;
@MediumTest
@@ -132,7 +131,7 @@ public class MediaOutputDialogTest extends SysuiTestCase {
mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
mMediaSessionManager, mLocalBluetoothManager, mStarter,
mNotifCollection, mDialogLaunchAnimator,
- Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+ mNearbyMediaDevicesManager, mAudioManager, mPowerExemptionManager,
mKeyguardManager, mFlags, mUserTracker);
mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
mMediaOutputDialog = makeTestDialog(mMediaOutputController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
index 01ffdcd580c0..ee3b80ac932e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
@@ -26,6 +26,7 @@ import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowMetrics
import androidx.core.view.WindowInsetsCompat.Type
+import androidx.lifecycle.LifecycleOwner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
@@ -41,9 +42,10 @@ import org.junit.Test
@SmallTest
class TaskPreviewSizeProviderTest : SysuiTestCase() {
- private val mockContext: Context = mock()
- private val resources: Resources = mock()
- private val windowManager: WindowManager = mock()
+ private val lifecycleOwner = mock<LifecycleOwner>()
+ private val mockContext = mock<Context>()
+ private val resources = mock<Resources>()
+ private val windowManager = mock<WindowManager>()
private val sizeUpdates = arrayListOf<Rect>()
private val testConfigurationController = FakeConfigurationController()
@@ -76,7 +78,7 @@ class TaskPreviewSizeProviderTest : SysuiTestCase() {
@Test
fun size_phoneDisplayAndRotate_emitsSizeUpdate() {
givenDisplay(width = 400, height = 600, isTablet = false)
- createSizeProvider()
+ createSizeProvider().also { it.onCreate(lifecycleOwner) }
givenDisplay(width = 600, height = 400, isTablet = false)
testConfigurationController.onConfigurationChanged(Configuration())
@@ -87,7 +89,7 @@ class TaskPreviewSizeProviderTest : SysuiTestCase() {
@Test
fun size_phoneDisplayAndRotateConfigurationChange_returnsUpdatedSize() {
givenDisplay(width = 400, height = 600, isTablet = false)
- val sizeProvider = createSizeProvider()
+ val sizeProvider = createSizeProvider().also { it.onCreate(lifecycleOwner) }
givenDisplay(width = 600, height = 400, isTablet = false)
testConfigurationController.onConfigurationChanged(Configuration())
@@ -95,6 +97,20 @@ class TaskPreviewSizeProviderTest : SysuiTestCase() {
assertThat(sizeProvider.size).isEqualTo(Rect(0, 0, 150, 100))
}
+ @Test
+ fun size_phoneDisplayAndRotateConfigurationChange_afterChooserDestroyed_doesNotUpdate() {
+ givenDisplay(width = 400, height = 600, isTablet = false)
+ val sizeProvider = createSizeProvider()
+ val previousSize = Rect(sizeProvider.size)
+
+ sizeProvider.onCreate(lifecycleOwner)
+ sizeProvider.onDestroy(lifecycleOwner)
+ givenDisplay(width = 600, height = 400, isTablet = false)
+ testConfigurationController.onConfigurationChanged(Configuration())
+
+ assertThat(sizeProvider.size).isEqualTo(previousSize)
+ }
+
private fun givenTaskbarSize(size: Int) {
val windowInsets =
WindowInsets.Builder()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
new file mode 100644
index 000000000000..f5a70f07c968
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.model
+
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class SysUiStateExtTest : SysuiTestCase() {
+
+ private val underTest = SysUiState(FakeDisplayTracker(context))
+
+ @Test
+ fun updateFlags() {
+ underTest.updateFlags(
+ Display.DEFAULT_DISPLAY,
+ 1 to true,
+ 2 to false,
+ 3 to true,
+ )
+
+ assertThat(underTest.flags and 1).isNotEqualTo(0)
+ assertThat(underTest.flags and 2).isEqualTo(0)
+ assertThat(underTest.flags and 3).isNotEqualTo(0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
deleted file mode 100644
index ceacaf9557ca..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.data.repository
-
-import android.content.Context
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.model.MultiShadeInteractionModel
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeConfig
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeRepositoryTest : SysuiTestCase() {
-
- private lateinit var inputProxy: MultiShadeInputProxy
-
- @Before
- fun setUp() {
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun proxiedInput() = runTest {
- val underTest = create()
- val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
-
- assertWithMessage("proxiedInput should start with null").that(latest).isNull()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
- assertThat(latest).isEqualTo(ProxiedInputModel.OnTap)
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f))
- assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f))
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f))
- assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f))
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd)
- }
-
- @Test
- fun shadeConfig_dualShadeEnabled() = runTest {
- overrideResource(R.bool.dual_shade_enabled, true)
- val underTest = create()
- val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
-
- assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java)
- }
-
- @Test
- fun shadeConfig_dualShadeNotEnabled() = runTest {
- overrideResource(R.bool.dual_shade_enabled, false)
- val underTest = create()
- val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig)
-
- assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java)
- }
-
- @Test
- fun forceCollapseAll() = runTest {
- val underTest = create()
- val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll)
-
- assertWithMessage("forceCollapseAll should start as false!")
- .that(forceCollapseAll)
- .isFalse()
-
- underTest.setForceCollapseAll(true)
- assertThat(forceCollapseAll).isTrue()
-
- underTest.setForceCollapseAll(false)
- assertThat(forceCollapseAll).isFalse()
- }
-
- @Test
- fun shadeInteraction() = runTest {
- val underTest = create()
- val shadeInteraction: MultiShadeInteractionModel? by
- collectLastValue(underTest.shadeInteraction)
-
- assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull()
-
- underTest.setShadeInteraction(
- MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)
- )
- assertThat(shadeInteraction)
- .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false))
-
- underTest.setShadeInteraction(
- MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)
- )
- assertThat(shadeInteraction)
- .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true))
-
- underTest.setShadeInteraction(null)
- assertThat(shadeInteraction).isNull()
- }
-
- @Test
- fun expansion() = runTest {
- val underTest = create()
- val leftExpansion: Float? by
- collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion })
- val rightExpansion: Float? by
- collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion })
- val singleExpansion: Float? by
- collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion })
-
- assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero()
- assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero()
- assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero()
-
- underTest.setExpansion(
- shadeId = ShadeId.LEFT,
- 0.4f,
- )
- assertThat(leftExpansion).isEqualTo(0.4f)
- assertThat(rightExpansion).isEqualTo(0f)
- assertThat(singleExpansion).isEqualTo(0f)
-
- underTest.setExpansion(
- shadeId = ShadeId.RIGHT,
- 0.73f,
- )
- assertThat(leftExpansion).isEqualTo(0.4f)
- assertThat(rightExpansion).isEqualTo(0.73f)
- assertThat(singleExpansion).isEqualTo(0f)
-
- underTest.setExpansion(
- shadeId = ShadeId.LEFT,
- 0.1f,
- )
- underTest.setExpansion(
- shadeId = ShadeId.SINGLE,
- 0.88f,
- )
- assertThat(leftExpansion).isEqualTo(0.1f)
- assertThat(rightExpansion).isEqualTo(0.73f)
- assertThat(singleExpansion).isEqualTo(0.88f)
- }
-
- private fun create(): MultiShadeRepository {
- return create(
- context = context,
- inputProxy = inputProxy,
- )
- }
-
- companion object {
- fun create(
- context: Context,
- inputProxy: MultiShadeInputProxy,
- ): MultiShadeRepository {
- return MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- )
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
deleted file mode 100644
index bcc99bc8dd0c..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import android.content.Context
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeInteractorTest : SysuiTestCase() {
-
- private lateinit var testScope: TestScope
- private lateinit var inputProxy: MultiShadeInputProxy
-
- @Before
- fun setUp() {
- testScope = TestScope()
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun maxShadeExpansion() =
- testScope.runTest {
- val underTest = create()
- val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion)
- assertWithMessage("maxShadeExpansion must start with 0.0!")
- .that(maxShadeExpansion)
- .isEqualTo(0f)
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
- assertThat(maxShadeExpansion).isEqualTo(0.441f)
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
- assertThat(maxShadeExpansion).isEqualTo(0.442f)
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
- assertThat(maxShadeExpansion).isEqualTo(0.441f)
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
- assertThat(maxShadeExpansion).isEqualTo(0f)
- }
-
- @Test
- fun isAnyShadeExpanded() =
- testScope.runTest {
- val underTest = create()
- val isAnyShadeExpanded: Boolean? by collectLastValue(underTest.isAnyShadeExpanded)
- assertWithMessage("isAnyShadeExpanded must start with false!")
- .that(isAnyShadeExpanded)
- .isFalse()
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f)
- assertThat(isAnyShadeExpanded).isTrue()
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f)
- assertThat(isAnyShadeExpanded).isTrue()
-
- underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f)
- assertThat(isAnyShadeExpanded).isTrue()
-
- underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f)
- assertThat(isAnyShadeExpanded).isFalse()
- }
-
- @Test
- fun isVisible_dualShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, true)
- val underTest = create()
- val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
- val isRightShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.RIGHT))
- val isSingleShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.SINGLE))
-
- assertThat(isLeftShadeVisible).isTrue()
- assertThat(isRightShadeVisible).isTrue()
- assertThat(isSingleShadeVisible).isFalse()
- }
-
- @Test
- fun isVisible_singleShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, false)
- val underTest = create()
- val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT))
- val isRightShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.RIGHT))
- val isSingleShadeVisible: Boolean? by
- collectLastValue(underTest.isVisible(ShadeId.SINGLE))
-
- assertThat(isLeftShadeVisible).isFalse()
- assertThat(isRightShadeVisible).isFalse()
- assertThat(isSingleShadeVisible).isTrue()
- }
-
- @Test
- fun isNonProxiedInputAllowed() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeNonProxiedInputAllowed: Boolean? by
- collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT))
- assertWithMessage("isNonProxiedInputAllowed should start as true!")
- .that(isLeftShadeNonProxiedInputAllowed)
- .isTrue()
-
- // Need to collect proxied input so the flows become hot as the gesture cancelation code
- // logic sits in side the proxiedInput flow for each shade.
- collectLastValue(underTest.proxiedInput(ShadeId.LEFT))
- collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
-
- // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
- // the
- // same shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
- )
- assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
-
- // Registering the end of the proxied interaction re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
-
- // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
- // disallowing non-proxied input on the LEFT shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
- )
- assertThat(isLeftShadeNonProxiedInputAllowed).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isLeftShadeNonProxiedInputAllowed).isTrue()
- }
-
- @Test
- fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
- // shade.
- underTest.onUserInteractionStarted(ShadeId.RIGHT)
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- underTest.onUserInteractionEnded(ShadeId.RIGHT)
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
- // shade.
- underTest.onUserInteractionStarted(ShadeId.LEFT)
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the LEFT shade re-allows it.
- underTest.onUserInteractionEnded(ShadeId.LEFT)
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
- }
-
- @Test
- fun collapseAll() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- underTest.collapseAll()
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isTrue()
-
- // Receiving proxied input on that's not a tap gesture, on the left-hand side resets the
- // "collapse all". Note that now the RIGHT shade is force-collapsed because we're
- // interacting with the LEFT shade.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f))
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isFalse()
- }
-
- @Test
- fun onTapOutside_collapsesAll() =
- testScope.runTest {
- val underTest = create()
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT))
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT))
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE))
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isTrue()
- }
-
- @Test
- fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
- testScope.runTest {
- val underTest = create()
- val proxiedInput: ProxiedInputModel? by
- collectLastValue(underTest.proxiedInput(ShadeId.RIGHT))
- underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT)
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNull()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
- assertThat(proxiedInput).isNull()
-
- underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT)
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNotNull()
- }
-
- private fun create(): MultiShadeInteractor {
- return create(
- testScope = testScope,
- context = context,
- inputProxy = inputProxy,
- )
- }
-
- companion object {
- fun create(
- testScope: TestScope,
- context: Context,
- inputProxy: MultiShadeInputProxy,
- ): MultiShadeInteractor {
- return MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository =
- MultiShadeRepositoryTest.create(
- context = context,
- inputProxy = inputProxy,
- ),
- inputProxy = inputProxy,
- )
- }
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
deleted file mode 100644
index 5890cbd06476..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt
+++ /dev/null
@@ -1,530 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.domain.interactor
-
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.TransitionStep
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.android.systemui.shade.ShadeController
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.currentTime
-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.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeMotionEventInteractorTest : SysuiTestCase() {
-
- private lateinit var underTest: MultiShadeMotionEventInteractor
-
- private lateinit var testScope: TestScope
- private lateinit var motionEvents: MutableSet<MotionEvent>
- private lateinit var repository: MultiShadeRepository
- private lateinit var interactor: MultiShadeInteractor
- private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
- private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
- private lateinit var falsingManager: FalsingManagerFake
- @Mock private lateinit var shadeController: ShadeController
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- testScope = TestScope()
- motionEvents = mutableSetOf()
-
- val inputProxy = MultiShadeInputProxy()
- repository =
- MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- )
- interactor =
- MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository = repository,
- inputProxy = inputProxy,
- )
- val featureFlags = FakeFeatureFlags()
- featureFlags.set(Flags.DUAL_SHADE, true)
- keyguardTransitionRepository = FakeKeyguardTransitionRepository()
- falsingManager = FalsingManagerFake()
-
- underTest =
- MultiShadeMotionEventInteractor(
- applicationContext = context,
- applicationScope = testScope.backgroundScope,
- multiShadeInteractor = interactor,
- featureFlags = featureFlags,
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- repository = keyguardTransitionRepository,
- )
- .keyguardTransitionInteractor,
- falsingManager = falsingManager,
- shadeController = shadeController,
- )
- }
-
- @After
- fun tearDown() {
- motionEvents.forEach { motionEvent -> motionEvent.recycle() }
- }
-
- @Test
- fun listenForIsAnyShadeExpanded_expanded_makesWindowViewVisible() =
- testScope.runTest {
- whenever(shadeController.isKeyguard).thenReturn(false)
- repository.setExpansion(ShadeId.LEFT, 0.1f)
- val expanded by collectLastValue(interactor.isAnyShadeExpanded)
- assertThat(expanded).isTrue()
-
- verify(shadeController).makeExpandedVisible(anyBoolean())
- }
-
- @Test
- fun listenForIsAnyShadeExpanded_collapsed_makesWindowViewInvisible() =
- testScope.runTest {
- whenever(shadeController.isKeyguard).thenReturn(false)
- repository.setForceCollapseAll(true)
- val expanded by collectLastValue(interactor.isAnyShadeExpanded)
- assertThat(expanded).isFalse()
-
- verify(shadeController).makeExpandedInvisible()
- }
-
- @Test
- fun listenForIsAnyShadeExpanded_collapsedOnKeyguard_makesWindowViewVisible() =
- testScope.runTest {
- whenever(shadeController.isKeyguard).thenReturn(true)
- repository.setForceCollapseAll(true)
- val expanded by collectLastValue(interactor.isAnyShadeExpanded)
- assertThat(expanded).isFalse()
-
- verify(shadeController).makeExpandedVisible(anyBoolean())
- }
-
- @Test
- fun shouldIntercept_initialDown_returnsFalse() =
- testScope.runTest {
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveBelowTouchSlop_returnsFalse() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop - 1f,
- )
- )
- )
- .isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlop_returnsTrue() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isTrue()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlop_butHorizontalFirst_returnsFalse() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- }
-
- @Test
- fun shouldIntercept_up_afterMovedAboveTouchSlop_returnsTrue() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
-
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isTrue()
- }
-
- @Test
- fun shouldIntercept_cancel_afterMovedAboveTouchSlop_returnsTrue() =
- testScope.runTest {
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f))
-
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isTrue()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndUp_butShadeExpanded_returnsFalse() =
- testScope.runTest {
- repository.setExpansion(ShadeId.LEFT, 0.1f)
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndCancel_butShadeExpanded_returnsFalse() =
- testScope.runTest {
- repository.setExpansion(ShadeId.LEFT, 0.1f)
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndUp_butBouncerShowing_returnsFalse() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.PRIMARY_BOUNCER,
- value = 0.1f,
- transitionState = TransitionState.STARTED,
- )
- )
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse()
- }
-
- @Test
- fun shouldIntercept_moveAboveTouchSlopAndCancel_butBouncerShowing_returnsFalse() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.PRIMARY_BOUNCER,
- value = 0.1f,
- transitionState = TransitionState.STARTED,
- )
- )
- runCurrent()
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
-
- assertThat(
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_MOVE,
- y = touchSlop + 1f,
- )
- )
- )
- .isFalse()
- assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse()
- }
-
- @Test
- fun tap_doesNotSendProxiedInput() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
-
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragBelowTouchSlop_doesNotSendProxiedInput() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop - 1f))
- underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))
-
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragShadeAboveTouchSlopAndUp() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 100f, // left shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = touchSlop + 1f
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 100f, // left shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput)
- .isEqualTo(
- ProxiedInputModel.OnDrag(
- xFraction = 0.1f,
- yDragAmountPx = yDragAmountPx,
- )
- )
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val upEvent = motionEvent(MotionEvent.ACTION_UP)
- assertThat(underTest.shouldIntercept(upEvent)).isTrue()
- underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragShadeAboveTouchSlopAndCancel() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 900f, // right shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = touchSlop + 1f
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 900f, // right shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isTrue()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput)
- .isEqualTo(
- ProxiedInputModel.OnDrag(
- xFraction = 0.9f,
- yDragAmountPx = yDragAmountPx,
- )
- )
- assertThat(singleShadeProxiedInput).isNull()
-
- val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
- assertThat(underTest.shouldIntercept(cancelEvent)).isTrue()
- underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragUp_withUp_doesNotShowShade() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 100f, // left shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = -(touchSlop + 1f) // dragging up
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 100f, // left shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val upEvent = motionEvent(MotionEvent.ACTION_UP)
- assertThat(underTest.shouldIntercept(upEvent)).isFalse()
- underTest.onTouchEvent(upEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- @Test
- fun dragUp_withCancel_falseTouch_showsThenHidesBouncer() =
- testScope.runTest {
- val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT))
- val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT))
- val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE))
-
- underTest.shouldIntercept(
- motionEvent(
- MotionEvent.ACTION_DOWN,
- x = 900f, // right shade
- )
- )
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- val yDragAmountPx = -(touchSlop + 1f) // drag up
- val moveEvent =
- motionEvent(
- MotionEvent.ACTION_MOVE,
- x = 900f, // right shade
- y = yDragAmountPx,
- )
- assertThat(underTest.shouldIntercept(moveEvent)).isFalse()
- underTest.onTouchEvent(moveEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
-
- falsingManager.setIsFalseTouch(true)
- val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL)
- assertThat(underTest.shouldIntercept(cancelEvent)).isFalse()
- underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000)
- assertThat(leftShadeProxiedInput).isNull()
- assertThat(rightShadeProxiedInput).isNull()
- assertThat(singleShadeProxiedInput).isNull()
- }
-
- private fun TestScope.motionEvent(
- action: Int,
- downTime: Long = currentTime,
- eventTime: Long = currentTime,
- x: Float = 0f,
- y: Float = 0f,
- ): MotionEvent {
- val motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0)
- motionEvents.add(motionEvent)
- return motionEvent
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt
deleted file mode 100644
index 893530982926..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.multishade.shared.math
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@SmallTest
-@RunWith(JUnit4::class)
-class MathTest : SysuiTestCase() {
-
- @Test
- fun isZero_zero_true() {
- assertThat(0f.isZero(epsilon = EPSILON)).isTrue()
- }
-
- @Test
- fun isZero_belowPositiveEpsilon_true() {
- assertThat((EPSILON * 0.999999f).isZero(epsilon = EPSILON)).isTrue()
- }
-
- @Test
- fun isZero_aboveNegativeEpsilon_true() {
- assertThat((EPSILON * -0.999999f).isZero(epsilon = EPSILON)).isTrue()
- }
-
- @Test
- fun isZero_positiveEpsilon_false() {
- assertThat(EPSILON.isZero(epsilon = EPSILON)).isFalse()
- }
-
- @Test
- fun isZero_negativeEpsilon_false() {
- assertThat((-EPSILON).isZero(epsilon = EPSILON)).isFalse()
- }
-
- @Test
- fun isZero_positive_false() {
- assertThat(1f.isZero(epsilon = EPSILON)).isFalse()
- }
-
- @Test
- fun isZero_negative_false() {
- assertThat((-1f).isZero(epsilon = EPSILON)).isFalse()
- }
-
- companion object {
- private const val EPSILON = 0.0001f
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
deleted file mode 100644
index 0484515e38bd..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class MultiShadeViewModelTest : SysuiTestCase() {
-
- private lateinit var testScope: TestScope
- private lateinit var inputProxy: MultiShadeInputProxy
-
- @Before
- fun setUp() {
- testScope = TestScope()
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun scrim_whenDualShadeCollapsed() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, true)
-
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
- }
-
- @Test
- fun scrim_whenDualShadeExpanded() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, true)
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
-
- underTest.leftShade.onExpansionChanged(0.5f)
- assertThat(scrimAlpha).isEqualTo(alpha * 0.5f)
- assertThat(isScrimEnabled).isTrue()
-
- underTest.rightShade.onExpansionChanged(1f)
- assertThat(scrimAlpha).isEqualTo(alpha * 1f)
- assertThat(isScrimEnabled).isTrue()
- }
-
- @Test
- fun scrim_whenSingleShadeCollapsed() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, false)
-
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
- }
-
- @Test
- fun scrim_whenSingleShadeExpanded() =
- testScope.runTest {
- val alpha = 0.5f
- overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
- overrideResource(R.bool.dual_shade_enabled, false)
- val underTest = create()
- val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
- val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
-
- underTest.singleShade.onExpansionChanged(0.95f)
-
- assertThat(scrimAlpha).isZero()
- assertThat(isScrimEnabled).isFalse()
- }
-
- private fun create(): MultiShadeViewModel {
- return MultiShadeViewModel(
- viewModelScope = testScope.backgroundScope,
- interactor =
- MultiShadeInteractorTest.create(
- testScope = testScope,
- context = context,
- inputProxy = inputProxy,
- ),
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
deleted file mode 100644
index e32aac596e5b..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.multishade.ui.viewmodel
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
-import com.android.systemui.multishade.shared.model.ProxiedInputModel
-import com.android.systemui.multishade.shared.model.ShadeId
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(JUnit4::class)
-class ShadeViewModelTest : SysuiTestCase() {
-
- private lateinit var testScope: TestScope
- private lateinit var inputProxy: MultiShadeInputProxy
- private var interactor: MultiShadeInteractor? = null
-
- @Before
- fun setUp() {
- testScope = TestScope()
- inputProxy = MultiShadeInputProxy()
- }
-
- @Test
- fun isVisible_dualShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, true)
- val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
- val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
- val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
-
- assertThat(isLeftShadeVisible).isTrue()
- assertThat(isRightShadeVisible).isTrue()
- assertThat(isSingleShadeVisible).isFalse()
- }
-
- @Test
- fun isVisible_singleShadeConfig() =
- testScope.runTest {
- overrideResource(R.bool.dual_shade_enabled, false)
- val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
- val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
- val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
-
- assertThat(isLeftShadeVisible).isFalse()
- assertThat(isRightShadeVisible).isFalse()
- assertThat(isSingleShadeVisible).isTrue()
- }
-
- @Test
- fun isSwipingEnabled() =
- testScope.runTest {
- val underTest = create(ShadeId.LEFT)
- val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled)
- assertWithMessage("isSwipingEnabled should start as true!")
- .that(isSwipingEnabled)
- .isTrue()
-
- // Need to collect proxied input so the flows become hot as the gesture cancelation code
- // logic sits in side the proxiedInput flow for each shade.
- collectLastValue(underTest.proxiedInput)
- collectLastValue(create(ShadeId.RIGHT).proxiedInput)
-
- // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
- // the
- // same shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
- )
- assertThat(isSwipingEnabled).isFalse()
-
- // Registering the end of the proxied interaction re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isSwipingEnabled).isTrue()
-
- // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
- // disallowing non-proxied input on the LEFT shade.
- inputProxy.onProxiedInput(
- ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
- )
- assertThat(isSwipingEnabled).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
- assertThat(isSwipingEnabled).isTrue()
- }
-
- @Test
- fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
- testScope.runTest {
- val leftShade = create(ShadeId.LEFT)
- val rightShade = create(ShadeId.RIGHT)
- val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed)
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(rightShade.isForceCollapsed)
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
- // shade.
- rightShade.onDragStarted()
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the RIGHT shade re-allows it.
- rightShade.onDragEnded()
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
- // shade.
- leftShade.onDragStarted()
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isFalse()
-
- // Registering the end of the interaction on the LEFT shade re-allows it.
- leftShade.onDragEnded()
- assertThat(isLeftShadeForceCollapsed).isFalse()
- assertThat(isRightShadeForceCollapsed).isFalse()
- assertThat(isSingleShadeForceCollapsed).isFalse()
- }
-
- @Test
- fun onTapOutside_collapsesAll() =
- testScope.runTest {
- val isLeftShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.LEFT).isForceCollapsed)
- val isRightShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.RIGHT).isForceCollapsed)
- val isSingleShadeForceCollapsed: Boolean? by
- collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
-
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isLeftShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isRightShadeForceCollapsed)
- .isFalse()
- assertWithMessage("isForceCollapsed should start as false!")
- .that(isSingleShadeForceCollapsed)
- .isFalse()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
- assertThat(isLeftShadeForceCollapsed).isTrue()
- assertThat(isRightShadeForceCollapsed).isTrue()
- assertThat(isSingleShadeForceCollapsed).isTrue()
- }
-
- @Test
- fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
- testScope.runTest {
- val underTest = create(ShadeId.RIGHT)
- val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
- underTest.onDragStarted()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNull()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
- assertThat(proxiedInput).isNull()
-
- underTest.onDragEnded()
-
- inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
- assertThat(proxiedInput).isNotNull()
- }
-
- private fun create(
- shadeId: ShadeId,
- ): ShadeViewModel {
- return ShadeViewModel(
- viewModelScope = testScope.backgroundScope,
- shadeId = shadeId,
- interactor = interactor
- ?: MultiShadeInteractorTest.create(
- testScope = testScope,
- context = context,
- inputProxy = inputProxy,
- )
- .also { interactor = it },
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 25d494cee5e8..cbfad56ed617 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -94,6 +94,7 @@ import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.shared.rotation.RotationButtonController;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
@@ -467,6 +468,7 @@ public class NavigationBarTest extends SysuiTestCase {
when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
return spy(new NavigationBar(
mNavigationBarView,
+ mock(ShadeController.class),
mNavigationBarFrame,
null,
context,
@@ -485,7 +487,7 @@ public class NavigationBarTest extends SysuiTestCase {
Optional.of(mock(Pip.class)),
Optional.of(mock(Recents.class)),
() -> Optional.of(mCentralSurfaces),
- mock(ShadeController.class),
+ mock(ShadeViewController.class),
mock(NotificationRemoteInputManager.class),
mock(NotificationShadeDepthController.class),
mHandler,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index 611c5b987d84..fab1de00dcbc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
@@ -29,6 +30,8 @@ import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.internal.util.LatencyTracker
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.plugins.NavigationEdgeBackPlugin
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -36,6 +39,8 @@ import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
@@ -58,6 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Mock private lateinit var latencyTracker: LatencyTracker
@Mock private lateinit var layoutParams: WindowManager.LayoutParams
@Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
+ private val featureFlags = FakeFeatureFlags()
@Before
fun setup() {
@@ -70,7 +76,8 @@ class BackPanelControllerTest : SysuiTestCase() {
Handler.createAsync(Looper.myLooper()),
vibratorHelper,
configurationController,
- latencyTracker
+ latencyTracker,
+ featureFlags
)
mBackPanelController.setLayoutParams(layoutParams)
mBackPanelController.setBackCallback(backCallback)
@@ -99,6 +106,7 @@ class BackPanelControllerTest : SysuiTestCase() {
@Test
fun handlesBackCommitted() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -122,7 +130,34 @@ class BackPanelControllerTest : SysuiTestCase() {
}
@Test
+ fun handlesBackCommitted_withOneWayHapticsAPI() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+ // Wait threshold duration and hold touch past trigger threshold
+ Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
+ continueTouch(START_X + touchSlop + triggerThreshold + 1)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.ACTIVE)
+ verify(backCallback).setTriggerBack(true)
+ testableLooper.moveTimeForward(100)
+ testableLooper.processAllMessages()
+ verify(vibratorHelper)
+ .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE))
+
+ finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1)
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.COMMITTED)
+ verify(backCallback).triggerBack()
+ }
+
+ @Test
fun handlesBackCancelled() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
startTouch()
// Move once to cross the touch slop
continueTouch(START_X + touchSlop.toFloat() + 1)
@@ -151,6 +186,38 @@ class BackPanelControllerTest : SysuiTestCase() {
verify(backCallback).cancelBack()
}
+ @Test
+ fun handlesBackCancelled_withOneWayHapticsAPI() {
+ featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
+ startTouch()
+ // Move once to cross the touch slop
+ continueTouch(START_X + touchSlop.toFloat() + 1)
+ // Move again to cross the back trigger threshold
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationTriggerThreshold
+ )
+ // Wait threshold duration and hold touch before trigger threshold
+ Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong())
+ continueTouch(
+ START_X + touchSlop + triggerThreshold -
+ mBackPanelController.params.deactivationTriggerThreshold
+ )
+ clearInvocations(backCallback)
+ Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
+ // Move in the opposite direction to cross the deactivation threshold and cancel back
+ continueTouch(START_X)
+
+ assertThat(mBackPanelController.currentState)
+ .isEqualTo(BackPanelController.GestureState.INACTIVE)
+ verify(backCallback).setTriggerBack(false)
+ verify(vibratorHelper)
+ .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE))
+
+ finishTouchActionUp(START_X)
+ verify(backCallback).cancelBack()
+ }
+
private fun startTouch() {
mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index a76af8e83248..c65a2d36e223 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -118,6 +118,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any()))
.thenReturn(NOTE_TASK_LONG_LABEL)
whenever(context.packageManager).thenReturn(packageManager)
+ whenever(context.createContextAsUser(any(), any())).thenReturn(context)
whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock())
whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL)
whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO)
@@ -353,7 +354,13 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
@Test
fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() {
- whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any()))
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
.thenReturn(10)
val user10 = UserHandle.of(/* userId= */ 10)
@@ -615,13 +622,21 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
- fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInWorkProfile() {
+ fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInTheUserSelectedUser() {
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(mainUserInfo.id)
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON)
- verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle)
+ verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle)
}
@Test
@@ -813,7 +828,15 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
- fun getUserForHandlingNotesTaking_cope_tailButton_shouldReturnWorkProfileUser() {
+ fun getUserForHandlingNotesTaking_cope_userSelectedWorkProfile_tailButton_shouldReturnWorkProfileUser() { // ktlint-disable max-line-length
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(workUserInfo.id)
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
@@ -823,6 +846,24 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
}
@Test
+ fun getUserForHandlingNotesTaking_cope_userSelectedMainProfile_tailButton_shouldReturnMainProfileUser() { // ktlint-disable max-line-length
+ whenever(
+ secureSettings.getIntForUser(
+ /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE),
+ /* def= */ any(),
+ /* userHandle= */ any()
+ )
+ )
+ .thenReturn(mainUserInfo.id)
+ whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
+ userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
+
+ val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON)
+
+ assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id))
+ }
+
+ @Test
fun getUserForHandlingNotesTaking_cope_appClip_shouldReturnCurrentUser() {
whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true)
userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
index 45bb9313264c..435a1f1327d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt
@@ -182,6 +182,32 @@ class PowerInteractorTest : SysuiTestCase() {
assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_APPLICATION)
}
+ @Test
+ fun wakeUpIfDreaming_dreaming_woken() {
+ // GIVEN device is dreaming
+ whenever(statusBarStateController.isDreaming).thenReturn(true)
+
+ // WHEN wakeUpIfDreaming is called
+ underTest.wakeUpIfDreaming("testReason", PowerManager.WAKE_REASON_GESTURE)
+
+ // THEN device is woken up
+ assertThat(repository.lastWakeWhy).isEqualTo("testReason")
+ assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE)
+ }
+
+ @Test
+ fun wakeUpIfDreaming_notDreaming_notWoken() {
+ // GIVEN device is not dreaming
+ whenever(statusBarStateController.isDreaming).thenReturn(false)
+
+ // WHEN wakeUpIfDreaming is called
+ underTest.wakeUpIfDreaming("why", PowerManager.WAKE_REASON_TAP)
+
+ // THEN device is not woken
+ assertThat(repository.lastWakeWhy).isNull()
+ assertThat(repository.lastWakeReason).isNull()
+ }
+
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
new file mode 100644
index 000000000000..0a8c0ab9817d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+import android.content.pm.UserInfo
+import android.os.Process.SYSTEM_UID
+import android.os.UserHandle
+import android.permission.PermissionGroupUsage
+import android.permission.PermissionManager
+import android.testing.AndroidTestingRunner
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.privacy.logging.PrivacyLogger
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PrivacyDialogControllerV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val USER_ID = 0
+ private const val ENT_USER_ID = 10
+
+ private const val TEST_PACKAGE_NAME = "test package name"
+ private const val TEST_ATTRIBUTION_TAG = "test attribution tag"
+ private const val TEST_PROXY_LABEL = "test proxy label"
+
+ private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA
+ private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE
+ private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION
+
+ private val TEST_INTENT = Intent("test_intent_action")
+ }
+
+ @Mock
+ private lateinit var dialog: PrivacyDialogV2
+ @Mock
+ private lateinit var permissionManager: PermissionManager
+ @Mock
+ private lateinit var packageManager: PackageManager
+ @Mock
+ private lateinit var privacyItemController: PrivacyItemController
+ @Mock
+ private lateinit var userTracker: UserTracker
+ @Mock
+ private lateinit var activityStarter: ActivityStarter
+ @Mock
+ private lateinit var privacyLogger: PrivacyLogger
+ @Mock
+ private lateinit var keyguardStateController: KeyguardStateController
+ @Mock
+ private lateinit var appOpsController: AppOpsController
+ @Captor
+ private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed>
+ @Captor
+ private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
+ @Captor
+ private lateinit var intentCaptor: ArgumentCaptor<Intent>
+ @Mock
+ private lateinit var uiEventLogger: UiEventLogger
+ @Mock
+ private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
+
+ private val backgroundExecutor = FakeExecutor(FakeSystemClock())
+ private val uiExecutor = FakeExecutor(FakeSystemClock())
+ private lateinit var controller: PrivacyDialogControllerV2
+ private var nextUid: Int = 0
+
+ private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider {
+ var list: List<PrivacyDialogV2.PrivacyElement>? = null
+ var manageApp: ((String, Int, Intent) -> Unit)? = null
+ var closeApp: ((String, Int) -> Unit)? = null
+ var openPrivacyDashboard: (() -> Unit)? = null
+
+ override fun makeDialog(
+ context: Context,
+ list: List<PrivacyDialogV2.PrivacyElement>,
+ manageApp: (String, Int, Intent) -> Unit,
+ closeApp: (String, Int) -> Unit,
+ openPrivacyDashboard: () -> Unit
+ ): PrivacyDialogV2 {
+ this.list = list
+ this.manageApp = manageApp
+ this.closeApp = closeApp
+ this.openPrivacyDashboard = openPrivacyDashboard
+ return dialog
+ }
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ nextUid = 0
+ setUpDefaultMockResponses()
+
+ controller = PrivacyDialogControllerV2(
+ permissionManager,
+ packageManager,
+ privacyItemController,
+ userTracker,
+ activityStarter,
+ backgroundExecutor,
+ uiExecutor,
+ privacyLogger,
+ keyguardStateController,
+ appOpsController,
+ uiEventLogger,
+ dialogLaunchAnimator,
+ dialogProvider
+ )
+ }
+
+ @After
+ fun tearDown() {
+ FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor)
+ dialogProvider.list = null
+ dialogProvider.manageApp = null
+ dialogProvider.closeApp = null
+ dialogProvider.openPrivacyDashboard = null
+ }
+
+ @Test
+ fun testMicMutedParameter() {
+ `when`(appOpsController.isMicMuted).thenReturn(true)
+ controller.showDialog(context)
+ backgroundExecutor.runAllReady()
+
+ verify(permissionManager).getIndicatorAppOpUsageData(true)
+ }
+
+ @Test
+ fun testPermissionManagerOnlyCalledInBackgroundThread() {
+ controller.showDialog(context)
+ verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean())
+ backgroundExecutor.runAllReady()
+ verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean())
+ }
+
+ @Test
+ fun testPackageManagerOnlyCalledInBackgroundThread() {
+ val usage = createMockPermGroupUsage()
+ `when`(usage.isPhoneCall).thenReturn(false)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ backgroundExecutor.runAllReady()
+ verify(packageManager, atLeastOnce())
+ .getApplicationInfoAsUser(anyString(), anyInt(), anyInt())
+ }
+
+ @Test
+ fun testShowDialogShowsDialogWithoutView() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), anyBoolean())
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testShowDialogShowsDialogWithView() {
+ val view = View(context)
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context, view)
+ exhaustExecutors()
+
+ verify(dialogLaunchAnimator).showFromView(dialog, view)
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testDontShowEmptyDialog() {
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testHideDialogDismissesDialogIfShown() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ controller.dismissDialog()
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopIfNotShown() {
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testHideDialogNoopAfterDismissed() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+ controller.dismissDialog()
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testShowForAllUsers() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+
+ exhaustExecutors()
+ verify(dialog).setShowForAllUsers(true)
+ }
+
+ @Test
+ fun testSingleElementInList() {
+ val usage = createMockPermGroupUsage(
+ packageName = TEST_PACKAGE_NAME,
+ uid = generateUidForUser(USER_ID),
+ permissionGroupName = PERM_CAMERA,
+ lastAccessTimeMillis = 5L,
+ isActive = true,
+ isPhoneCall = false,
+ attributionTag = null,
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA)
+ assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).userId).isEqualTo(USER_ID)
+ assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME)
+ assertThat(list.get(0).attributionTag).isNull()
+ assertThat(list.get(0).attributionLabel).isNull()
+ assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL)
+ assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L)
+ assertThat(list.get(0).isActive).isTrue()
+ assertThat(list.get(0).isPhoneCall).isFalse()
+ assertThat(list.get(0).isService).isFalse()
+ assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA)
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ }
+ }
+
+ private fun isIntentEqual(actual: Intent, expected: Intent): Boolean {
+ return actual.action == expected.action &&
+ actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) ==
+ expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) &&
+ actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle ==
+ expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle
+ }
+
+ @Test
+ fun testTwoElementsDifferentType_sorted() {
+ val usage_camera = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_camera",
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_microphone",
+ permissionGroupName = PERM_MICROPHONE
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_microphone, usage_camera)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(list).hasSize(2)
+ assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0)
+ }
+ }
+
+ @Test
+ fun testTwoElementsSameType_oneActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true
+ )
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_active)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.isActive).isTrue()
+ }
+
+ @Test
+ fun testTwoElementsSameType_twoActive() {
+ val usage_active = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active",
+ isActive = true,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_active_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_active_recent",
+ isActive = true,
+ lastAccessTimeMillis = 1L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_active, usage_active_moreRecent)
+ )
+ controller.showDialog(context)
+ exhaustExecutors()
+ assertThat(dialogProvider.list).hasSize(2)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L)
+ assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L)
+ }
+
+ @Test
+ fun testManyElementsSameType_bothRecent() {
+ val usage_recent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_recent",
+ isActive = false,
+ lastAccessTimeMillis = 0L
+ )
+ val usage_moreRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_moreRecent",
+ isActive = false,
+ lastAccessTimeMillis = 1L
+ )
+ val usage_mostRecent = createMockPermGroupUsage(
+ packageName = "${TEST_PACKAGE_NAME}_mostRecent",
+ isActive = false,
+ lastAccessTimeMillis = 2L
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_recent, usage_mostRecent, usage_moreRecent)
+ )
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L)
+ }
+
+ @Test
+ fun testMicAndCameraDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(1)
+ assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+
+ @Test
+ fun testLocationDisabled() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(2)
+ dialogProvider.list?.forEach {
+ assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION)
+ }
+ }
+
+ @Test
+ fun testAllIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ assertThat(dialogProvider.list).hasSize(3)
+ }
+
+ @Test
+ fun testNoIndicatorsAvailable() {
+ val usage_camera = createMockPermGroupUsage(
+ permissionGroupName = PERM_CAMERA
+ )
+ val usage_microphone = createMockPermGroupUsage(
+ permissionGroupName = PERM_MICROPHONE
+ )
+ val usage_location = createMockPermGroupUsage(
+ permissionGroupName = PERM_LOCATION
+ )
+
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(
+ listOf(usage_camera, usage_location, usage_microphone)
+ )
+ `when`(privacyItemController.micCameraAvailable).thenReturn(false)
+ `when`(privacyItemController.locationAvailable).thenReturn(false)
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testNotCurrentUser() {
+ val usage_other = createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID + 1)
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean()))
+ .thenReturn(listOf(usage_other))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog, never()).show()
+ }
+
+ @Test
+ fun testStartActivitySuccess() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP)
+
+ verify(dialog).dismiss()
+ }
+
+ @Test
+ fun testStartActivityFailure() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor))
+
+ activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED)
+
+ verify(dialog, never()).dismiss()
+ }
+
+ @Test
+ fun testCallOnSecondaryUser() {
+ // Calls happen in
+ val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(ENT_USER_ID, "", 0)
+ ))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).show()
+ }
+
+ @Test
+ fun testManageAppLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testCloseAppLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.closeApp?.invoke(TEST_PACKAGE_NAME, USER_ID)
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP,
+ USER_ID, TEST_PACKAGE_NAME)
+ }
+
+ @Test
+ fun testOpenPrivacyDashboardLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.openPrivacyDashboard?.invoke()
+ verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD)
+ }
+
+ @Test
+ fun testDismissedDialogLogs() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor))
+
+ dialogDismissedCaptor.value.onDialogDismissed()
+
+ controller.dismissDialog()
+
+ verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED)
+ }
+
+ @Test
+ fun testDefaultIntent() {
+ val usage = createMockPermGroupUsage()
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnEnterpriseUser() {
+ val usage =
+ createMockPermGroupUsage(
+ uid = generateUidForUser(ENT_USER_ID),
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(
+ TEST_PACKAGE_NAME, ENT_USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnInvalidAttributionTag() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = "INVALID_ATTRIBUTION_TAG",
+ proxyLabel = TEST_PROXY_LABEL
+ )
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testServiceIntentOnCorrectSubAttribution() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG,
+ attributionLabel = "TEST_LABEL"
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ val navigationIntent = list.get(0).navigationIntent!!
+ assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE)
+ assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME))
+ .isEqualTo(PERM_CAMERA)
+ assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS))
+ .isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString()))
+ assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false))
+ .isTrue()
+ assertThat(list.get(0).isService).isTrue()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnMissingAttributionLabel() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo()
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ @Test
+ fun testDefaultIntentOnIncorrectPermission() {
+ val usage = createMockPermGroupUsage(
+ attributionTag = TEST_ATTRIBUTION_TAG
+ )
+
+ val activityInfo = createMockActivityInfo(
+ permission = "INCORRECT_PERMISSION"
+ )
+ val resolveInfo = createMockResolveInfo(activityInfo)
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
+ `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>()))
+ .thenAnswer { resolveInfo }
+ controller.showDialog(context)
+ exhaustExecutors()
+
+ dialogProvider.list?.let { list ->
+ assertThat(isIntentEqual(list.get(0).navigationIntent!!,
+ controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID)))
+ .isTrue()
+ assertThat(list.get(0).isService).isFalse()
+ }
+ }
+
+ private fun exhaustExecutors() {
+ FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor)
+ }
+
+ private fun setUpDefaultMockResponses() {
+ `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList())
+ `when`(appOpsController.isMicMuted).thenReturn(false)
+
+ `when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenAnswer { FakeApplicationInfo(it.getArgument(0)) }
+
+ `when`(privacyItemController.locationAvailable).thenReturn(true)
+ `when`(privacyItemController.micCameraAvailable).thenReturn(true)
+
+ `when`(userTracker.userProfiles).thenReturn(listOf(
+ UserInfo(USER_ID, "", 0),
+ UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE)
+ ))
+
+ `when`(keyguardStateController.isUnlocked).thenReturn(true)
+ }
+
+ private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return label
+ }
+ }
+
+ private fun generateUidForUser(user: Int): Int {
+ return user * UserHandle.PER_USER_RANGE + nextUid++
+ }
+
+ private fun createMockResolveInfo(
+ activityInfo: ActivityInfo? = null
+ ): ResolveInfo {
+ val resolveInfo = mock(ResolveInfo::class.java)
+ resolveInfo.activityInfo = activityInfo
+ return resolveInfo
+ }
+
+ private fun createMockActivityInfo(
+ permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE,
+ className: String = "TEST_CLASS_NAME"
+ ): ActivityInfo {
+ val activityInfo = mock(ActivityInfo::class.java)
+ activityInfo.permission = permission
+ activityInfo.name = className
+ return activityInfo
+ }
+
+ private fun createMockPermGroupUsage(
+ packageName: String = TEST_PACKAGE_NAME,
+ uid: Int = generateUidForUser(USER_ID),
+ permissionGroupName: String = PERM_CAMERA,
+ lastAccessTimeMillis: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null
+ ): PermissionGroupUsage {
+ val usage = mock(PermissionGroupUsage::class.java)
+ `when`(usage.packageName).thenReturn(packageName)
+ `when`(usage.uid).thenReturn(uid)
+ `when`(usage.permissionGroupName).thenReturn(permissionGroupName)
+ `when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis)
+ `when`(usage.isActive).thenReturn(isActive)
+ `when`(usage.isPhoneCall).thenReturn(isPhoneCall)
+ `when`(usage.attributionTag).thenReturn(attributionTag)
+ `when`(usage.attributionLabel).thenReturn(attributionLabel)
+ `when`(usage.proxyLabel).thenReturn(proxyLabel)
+ return usage
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
new file mode 100644
index 000000000000..f4644a578d24
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.privacy
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class PrivacyDialogV2Test : SysuiTestCase() {
+
+ companion object {
+ private const val TEST_PACKAGE_NAME = "test_pkg"
+ private const val TEST_USER_ID = 0
+ private const val TEST_PERM_GROUP = "test_perm_group"
+
+ private val TEST_INTENT = Intent("test_intent_action")
+
+ private fun createPrivacyElement(
+ type: PrivacyType = PrivacyType.TYPE_MICROPHONE,
+ packageName: String = TEST_PACKAGE_NAME,
+ userId: Int = TEST_USER_ID,
+ applicationName: CharSequence = "App",
+ attributionTag: CharSequence? = null,
+ attributionLabel: CharSequence? = null,
+ proxyLabel: CharSequence? = null,
+ lastActiveTimestamp: Long = 0L,
+ isActive: Boolean = false,
+ isPhoneCall: Boolean = false,
+ isService: Boolean = false,
+ permGroupName: String = TEST_PERM_GROUP,
+ navigationIntent: Intent = TEST_INTENT
+ ) =
+ PrivacyDialogV2.PrivacyElement(
+ type,
+ packageName,
+ userId,
+ applicationName,
+ attributionTag,
+ attributionLabel,
+ proxyLabel,
+ lastActiveTimestamp,
+ isActive,
+ isPhoneCall,
+ isService,
+ permGroupName,
+ navigationIntent
+ )
+ }
+
+ @Mock private lateinit var manageApp: (String, Int, Intent) -> Unit
+ @Mock private lateinit var closeApp: (String, Int) -> Unit
+ @Mock private lateinit var openPrivacyDashboard: () -> Unit
+ private lateinit var dialog: PrivacyDialogV2
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @After
+ fun teardown() {
+ if (this::dialog.isInitialized) {
+ dialog.dismiss()
+ }
+ }
+
+ @Test
+ fun testManageAppCalledWithCorrectParams() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_manage_app_button).callOnClick()
+
+ verify(manageApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, TEST_INTENT)
+ }
+
+ @Test
+ fun testCloseAppCalledWithCorrectParams() {
+ val list = listOf(createPrivacyElement(isActive = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_close_app_button).callOnClick()
+
+ verify(closeApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID)
+ }
+
+ @Test
+ fun testCloseAppMissingForService() {
+ val list = listOf(createPrivacyElement(isActive = true, isService = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.findViewById<View>(R.id.privacy_dialog_manage_app_button)).isNotNull()
+ assertThat(dialog.findViewById<View>(R.id.privacy_dialog_close_app_button)).isNull()
+ }
+
+ @Test
+ fun testMoreButton() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_more_button).callOnClick()
+
+ verify(openPrivacyDashboard).invoke()
+ }
+
+ @Test
+ fun testCloseButton() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+ verify(dismissListener, never()).onDialogDismissed()
+
+ dialog.requireViewById<View>(R.id.privacy_dialog_close_button).callOnClick()
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledOnDismiss() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.addOnDismissListener(dismissListener)
+ dialog.show()
+ verify(dismissListener, never()).onDialogDismissed()
+
+ dialog.dismiss()
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java)
+ dialog.show()
+ dialog.dismiss()
+
+ dialog.addOnDismissListener(dismissListener)
+
+ verify(dismissListener).onDialogDismissed()
+ }
+
+ @Test
+ fun testCorrectNumElements() {
+ val list =
+ listOf(
+ createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true),
+ createPrivacyElement()
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(
+ dialog.requireViewById<ViewGroup>(R.id.privacy_dialog_items_container).childCount
+ )
+ .isEqualTo(2)
+ }
+
+ @Test
+ fun testHeaderText() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_title).text)
+ .isEqualTo(TEST_PERM_GROUP)
+ }
+
+ @Test
+ fun testUsingText() {
+ val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App")
+ }
+
+ @Test
+ fun testRecentText() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used by App")
+ }
+
+ @Test
+ fun testPhoneCall() {
+ val list = listOf(createPrivacyElement(isPhoneCall = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used in phone call")
+ }
+
+ @Test
+ fun testPhoneCallNotClickable() {
+ val list = listOf(createPrivacyElement(isPhoneCall = true))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<View>(R.id.privacy_dialog_item_card).isClickable)
+ .isFalse()
+ assertThat(
+ dialog
+ .requireViewById<View>(R.id.privacy_dialog_item_header_expand_toggle)
+ .visibility
+ )
+ .isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testProxyLabel() {
+ val list = listOf(createPrivacyElement(proxyLabel = "proxy label"))
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("Recently used by App (proxy label)")
+ }
+
+ @Test
+ fun testSubattribution() {
+ val list =
+ listOf(
+ createPrivacyElement(
+ attributionLabel = "For subattribution",
+ isActive = true,
+ isService = true
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+
+ dialog.show()
+
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App (For subattribution)")
+ }
+
+ @Test
+ fun testSubattributionAndProxyLabel() {
+ val list =
+ listOf(
+ createPrivacyElement(
+ attributionLabel = "For subattribution",
+ proxyLabel = "proxy label",
+ isActive = true
+ )
+ )
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+ assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text)
+ .isEqualTo("In use by App (For subattribution \u2022 proxy label)")
+ }
+
+ @Test
+ fun testDialogHasTitle() {
+ val list = listOf(createPrivacyElement())
+ dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ assertThat(dialog.window?.attributes?.title).isEqualTo("Microphone & Camera")
+ }
+
+ @Test
+ fun testDialogIsFullscreen() {
+ dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard)
+ dialog.show()
+
+ assertThat(dialog.window?.attributes?.width).isEqualTo(MATCH_PARENT)
+ assertThat(dialog.window?.attributes?.height).isEqualTo(MATCH_PARENT)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
index 3620233fc9df..fa02e8cb3e54 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
@@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpsController
import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.statusbar.phone.StatusIconContainer
@@ -24,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
@@ -54,6 +58,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
@Mock
private lateinit var privacyDialogController: PrivacyDialogController
@Mock
+ private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2
+ @Mock
private lateinit var privacyLogger: PrivacyLogger
@Mock
private lateinit var iconContainer: StatusIconContainer
@@ -69,6 +75,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
private lateinit var safetyCenterManager: SafetyCenterManager
@Mock
private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock
+ private lateinit var featureFlags: FeatureFlags
private val uiExecutor = FakeExecutor(FakeSystemClock())
private val backgroundExecutor = FakeExecutor(FakeSystemClock())
@@ -94,6 +102,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
uiEventLogger,
privacyChip,
privacyDialogController,
+ privacyDialogControllerV2,
privacyLogger,
iconContainer,
permissionManager,
@@ -103,7 +112,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
appOpsController,
broadcastDispatcher,
safetyCenterManager,
- deviceProvisionedController
+ deviceProvisionedController,
+ featureFlags
)
backgroundExecutor.runAllReady()
@@ -154,17 +164,55 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
}
@Test
- fun testPrivacyChipClicked() {
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false)
controller.onParentVisible()
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
+ }
+
+ @Test
+ fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false)
+ val receiverCaptor = argumentCaptor<BroadcastReceiver>()
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
+ verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
+ any(), any(), nullable(), anyInt(), nullable())
+ receiverCaptor.value.onReceive(
+ context,
+ Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
+ )
+ backgroundExecutor.runAllReady()
+ controller.onParentVisible()
+ val captor = argumentCaptor<View.OnClickListener>()
+ verify(privacyChip).setOnClickListener(capture(captor))
+ captor.value.onClick(privacyChip)
+ verify(privacyDialogController, never()).showDialog(any(Context::class.java))
+ verify(privacyDialogControllerV2, never())
+ .showDialog(any(Context::class.java), any(View::class.java))
}
@Test
- fun testSafetyCenterFlag() {
+ fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() {
+ whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true)
val receiverCaptor = argumentCaptor<BroadcastReceiver>()
whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor),
@@ -178,6 +226,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() {
val captor = argumentCaptor<View.OnClickListener>()
verify(privacyChip).setOnClickListener(capture(captor))
captor.value.onClick(privacyChip)
+ verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip))
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index a0d8f98a4ad1..9d9d0c7de2ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -154,6 +154,21 @@ class QSPanelControllerTest : SysuiTestCase() {
verify(qsPanel).setCanCollapse(true)
}
+ @Test
+ fun multipleListeningOnlyCallsBrightnessControllerOnce() {
+ controller.setListening(true, true)
+ controller.setListening(true, false)
+ controller.setListening(true, true)
+
+ verify(brightnessController).registerCallbacks()
+
+ controller.setListening(false, true)
+ controller.setListening(false, false)
+ controller.setListening(false, true)
+
+ verify(brightnessController).unregisterCallbacks()
+ }
+
private fun setShouldUseSplitShade(shouldUse: Boolean) {
testableResources.addOverride(R.bool.config_use_split_notification_shade, shouldUse)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 355c4b667333..49ece66e0cfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -29,6 +29,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.ScreenLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -38,6 +39,7 @@ import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.recents.OverviewProxyService.ACTION_QUICKSTEP
import com.android.systemui.settings.FakeDisplayTracker
import com.android.systemui.settings.UserTracker
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shared.recents.IOverviewProxy
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK
import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP
@@ -48,11 +50,11 @@ import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.sysui.ShellInterface
import com.google.common.util.concurrent.MoreExecutors
-import dagger.Lazy
import java.util.Optional
import java.util.concurrent.Executor
import org.junit.After
@@ -81,6 +83,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker)
+ private val featureFlags = FakeFeatureFlags()
private val screenLifecycle = ScreenLifecycle(dumpManager)
private val wakefulnessLifecycle =
WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
@@ -93,6 +96,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
@Mock private lateinit var shellInterface: ShellInterface
@Mock private lateinit var navBarController: NavigationBarController
@Mock private lateinit var centralSurfaces: CentralSurfaces
+ @Mock private lateinit var shadeViewController: ShadeViewController
@Mock private lateinit var navModeController: NavigationModeController
@Mock private lateinit var statusBarWinController: NotificationShadeWindowController
@Mock private lateinit var userTracker: UserTracker
@@ -130,11 +134,13 @@ class OverviewProxyServiceTest : SysuiTestCase() {
executor,
commandQueue,
shellInterface,
- Lazy { navBarController },
- Lazy { Optional.of(centralSurfaces) },
+ { navBarController },
+ { Optional.of(centralSurfaces) },
+ { shadeViewController },
navModeController,
statusBarWinController,
sysUiState,
+ mock(),
userTracker,
screenLifecycle,
wakefulnessLifecycle,
@@ -142,6 +148,7 @@ class OverviewProxyServiceTest : SysuiTestCase() {
displayTracker,
sysuiUnlockAnimationController,
assistUtils,
+ featureFlags,
dumpManager,
unfoldTransitionProgressForwarder
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 3050c4edd24f..d2bbfa85604b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -100,4 +100,15 @@ class SceneInteractorTest : SysuiTestCase() {
)
)
}
+
+ @Test
+ fun remoteUserInput() = runTest {
+ val remoteUserInput by collectLastValue(underTest.remoteUserInput)
+ assertThat(remoteUserInput).isNull()
+
+ for (input in SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE) {
+ underTest.onRemoteUserInput(input)
+ assertThat(remoteUserInput).isEqualTo(input)
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index 5638d708cf30..6f6c5a589f44 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.scene.domain.startable
+import android.view.Display
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -23,17 +26,24 @@ import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
+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.junit.runners.JUnit4
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(JUnit4::class)
@@ -53,6 +63,7 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
utils.keyguardInteractor(
repository = keyguardRepository,
)
+ private val sysUiState: SysUiState = mock()
private val underTest =
SystemUiDefaultSceneContainerStartable(
@@ -61,6 +72,8 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
authenticationInteractor = authenticationInteractor,
keyguardInteractor = keyguardInteractor,
featureFlags = featureFlags,
+ sysUiState = sysUiState,
+ displayId = Display.DEFAULT_DISPLAY,
)
@Before
@@ -375,6 +388,31 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
}
+ @Test
+ fun hydrateSystemUiState() =
+ testScope.runTest {
+ underTest.start()
+ runCurrent()
+ clearInvocations(sysUiState)
+
+ listOf(
+ SceneKey.Gone,
+ SceneKey.Lockscreen,
+ SceneKey.Bouncer,
+ SceneKey.Shade,
+ SceneKey.QuickSettings,
+ )
+ .forEachIndexed { index, sceneKey ->
+ sceneInteractor.setCurrentScene(
+ SceneContainerNames.SYSTEM_UI_DEFAULT,
+ SceneModel(sceneKey),
+ )
+ runCurrent()
+
+ verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
+ }
+ }
+
private fun prepareState(
isFeatureEnabled: Boolean = true,
isDeviceUnlocked: Boolean = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 6882be7fe184..63ea918c904a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,14 +18,19 @@
package com.android.systemui.scene.ui.viewmodel
+import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -68,4 +73,35 @@ class SceneContainerViewModelTest : SysuiTestCase() {
underTest.setCurrentScene(SceneModel(SceneKey.Shade))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade))
}
+
+ @Test
+ fun onRemoteUserInput() = runTest {
+ val remoteUserInput by collectLastValue(underTest.remoteUserInput)
+ assertThat(remoteUserInput).isNull()
+
+ val inputs =
+ SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE.map { remoteUserInputToMotionEvent(it) }
+
+ inputs.forEachIndexed { index, input ->
+ underTest.onRemoteUserInput(input)
+ assertThat(remoteUserInput).isEqualTo(SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE[index])
+ }
+ }
+
+ private fun TestScope.remoteUserInputToMotionEvent(input: RemoteUserInput): MotionEvent {
+ return MotionEvent.obtain(
+ currentTime,
+ currentTime,
+ when (input.action) {
+ RemoteUserInputAction.DOWN -> MotionEvent.ACTION_DOWN
+ RemoteUserInputAction.MOVE -> MotionEvent.ACTION_MOVE
+ RemoteUserInputAction.UP -> MotionEvent.ACTION_UP
+ RemoteUserInputAction.CANCEL -> MotionEvent.ACTION_CANCEL
+ RemoteUserInputAction.UNKNOWN -> MotionEvent.ACTION_OUTSIDE
+ },
+ input.x,
+ input.y,
+ 0
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
new file mode 100644
index 000000000000..2b7840533df2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings.brightness
+
+import android.hardware.display.DisplayManager
+import android.os.Handler
+import android.service.vr.IVrManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.DisplayTracker
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class BrightnessControllerTest : SysuiTestCase() {
+
+ private val executor = FakeExecutor(FakeSystemClock())
+ private val secureSettings = FakeSettings()
+ @Mock private lateinit var toggleSlider: ToggleSlider
+ @Mock private lateinit var userTracker: UserTracker
+ @Mock private lateinit var displayTracker: DisplayTracker
+ @Mock private lateinit var displayManager: DisplayManager
+ @Mock private lateinit var iVrManager: IVrManager
+
+ private lateinit var testableLooper: TestableLooper
+
+ private lateinit var underTest: BrightnessController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+
+ underTest =
+ BrightnessController(
+ context,
+ toggleSlider,
+ userTracker,
+ displayTracker,
+ displayManager,
+ secureSettings,
+ iVrManager,
+ executor,
+ mock(),
+ Handler(testableLooper.looper)
+ )
+ }
+
+ @Test
+ fun registerCallbacksMultipleTimes_onlyOneRegistration() {
+ val repeats = 100
+ repeat(repeats) { underTest.registerCallbacks() }
+ val messagesProcessed = testableLooper.processMessagesNonBlocking(repeats)
+
+ verify(displayTracker).addBrightnessChangeCallback(any(), any())
+ verify(iVrManager).registerListener(any())
+
+ assertThat(messagesProcessed).isEqualTo(1)
+ }
+
+ @Test
+ fun unregisterCallbacksMultipleTimes_onlyOneUnregistration() {
+ val repeats = 100
+ underTest.registerCallbacks()
+ testableLooper.processAllMessages()
+
+ repeat(repeats) { underTest.unregisterCallbacks() }
+ val messagesProcessed = testableLooper.processMessagesNonBlocking(repeats)
+
+ verify(displayTracker).removeCallback(any())
+ verify(iVrManager).unregisterListener(any())
+
+ assertThat(messagesProcessed).isEqualTo(1)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index 5c35913f6e20..ed1397ff7013 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -18,7 +18,6 @@ package com.android.systemui.settings.brightness
import android.content.Intent
import android.graphics.Rect
-import android.os.Handler
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -29,8 +28,6 @@ import androidx.test.rule.ActivityTestRule
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.SingleActivityFactory
-import com.android.systemui.settings.FakeDisplayTracker
-import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.concurrency.FakeExecutor
@@ -53,28 +50,24 @@ import org.mockito.MockitoAnnotations
@TestableLooper.RunWithLooper
class BrightnessDialogTest : SysuiTestCase() {
- @Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory
- @Mock private lateinit var backgroundHandler: Handler
@Mock private lateinit var brightnessSliderController: BrightnessSliderController
+ @Mock private lateinit var brightnessControllerFactory: BrightnessController.Factory
+ @Mock private lateinit var brightnessController: BrightnessController
@Mock private lateinit var accessibilityMgr: AccessibilityManagerWrapper
private val clock = FakeSystemClock()
private val mainExecutor = FakeExecutor(clock)
- private var displayTracker = FakeDisplayTracker(mContext)
-
@Rule
@JvmField
var activityRule =
ActivityTestRule(
/* activityFactory= */ SingleActivityFactory {
TestDialog(
- userTracker,
- displayTracker,
brightnessSliderControllerFactory,
+ brightnessControllerFactory,
mainExecutor,
- backgroundHandler,
accessibilityMgr
)
},
@@ -88,6 +81,7 @@ class BrightnessDialogTest : SysuiTestCase() {
`when`(brightnessSliderControllerFactory.create(any(), any()))
.thenReturn(brightnessSliderController)
`when`(brightnessSliderController.rootView).thenReturn(View(context))
+ `when`(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
}
@After
@@ -178,19 +172,15 @@ class BrightnessDialogTest : SysuiTestCase() {
}
class TestDialog(
- userTracker: UserTracker,
- displayTracker: FakeDisplayTracker,
brightnessSliderControllerFactory: BrightnessSliderController.Factory,
+ brightnessControllerFactory: BrightnessController.Factory,
mainExecutor: DelayableExecutor,
- backgroundHandler: Handler,
accessibilityMgr: AccessibilityManagerWrapper
) :
BrightnessDialog(
- userTracker,
- displayTracker,
brightnessSliderControllerFactory,
+ brightnessControllerFactory,
mainExecutor,
- backgroundHandler,
accessibilityMgr
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt
new file mode 100644
index 000000000000..24d62fba8471
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.os.PowerManager
+import android.testing.AndroidTestingRunner
+import android.view.MotionEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+class LockscreenHostedDreamGestureListenerTest : SysuiTestCase() {
+ @Mock private lateinit var falsingManager: FalsingManager
+ @Mock private lateinit var falsingCollector: FalsingCollector
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+ @Mock private lateinit var shadeLogger: ShadeLogger
+ @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController
+ @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private lateinit var powerRepository: FakePowerRepository
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var underTest: LockscreenHostedDreamGestureListener
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ powerRepository = FakePowerRepository()
+ keyguardRepository = FakeKeyguardRepository()
+
+ underTest =
+ LockscreenHostedDreamGestureListener(
+ falsingManager,
+ PowerInteractor(
+ powerRepository,
+ keyguardRepository,
+ falsingCollector,
+ screenOffAnimationController,
+ statusBarStateController,
+ ),
+ statusBarStateController,
+ primaryBouncerInteractor,
+ keyguardRepository,
+ shadeLogger,
+ )
+ whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+ whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(false)
+ }
+
+ @Test
+ fun testGestureDetector_onSingleTap_whileDreaming() =
+ testScope.runTest {
+ // GIVEN device dreaming and the dream is hosted in lockscreen
+ whenever(statusBarStateController.isDreaming).thenReturn(true)
+ keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+ testScope.runCurrent()
+
+ // GIVEN the falsing manager does NOT think the tap is a false tap
+ whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+ // WHEN there's a tap
+ underTest.onSingleTapUp(upEv)
+
+ // THEN wake up device if dreaming
+ Truth.assertThat(powerRepository.lastWakeWhy).isNotNull()
+ Truth.assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_TAP)
+ }
+
+ @Test
+ fun testGestureDetector_onSingleTap_notOnKeyguard() =
+ testScope.runTest {
+ // GIVEN device dreaming and the dream is hosted in lockscreen
+ whenever(statusBarStateController.isDreaming).thenReturn(true)
+ keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+ testScope.runCurrent()
+
+ // GIVEN shade is open
+ whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+
+ // GIVEN the falsing manager does NOT think the tap is a false tap
+ whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+ // WHEN there's a tap
+ underTest.onSingleTapUp(upEv)
+
+ // THEN the falsing manager never gets a call
+ verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+ }
+
+ @Test
+ fun testGestureDetector_onSingleTap_bouncerShown() =
+ testScope.runTest {
+ // GIVEN device dreaming and the dream is hosted in lockscreen
+ whenever(statusBarStateController.isDreaming).thenReturn(true)
+ keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+ testScope.runCurrent()
+
+ // GIVEN bouncer is expanded
+ whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(true)
+
+ // GIVEN the falsing manager does NOT think the tap is a false tap
+ whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false)
+
+ // WHEN there's a tap
+ underTest.onSingleTapUp(upEv)
+
+ // THEN the falsing manager never gets a call
+ verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+ }
+
+ @Test
+ fun testGestureDetector_onSingleTap_falsing() =
+ testScope.runTest {
+ // GIVEN device dreaming and the dream is hosted in lockscreen
+ whenever(statusBarStateController.isDreaming).thenReturn(true)
+ keyguardRepository.setIsActiveDreamLockscreenHosted(true)
+ testScope.runCurrent()
+
+ // GIVEN the falsing manager thinks the tap is a false tap
+ whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(true)
+
+ // WHEN there's a tap
+ underTest.onSingleTapUp(upEv)
+
+ // THEN the device doesn't wake up
+ Truth.assertThat(powerRepository.lastWakeWhy).isNull()
+ Truth.assertThat(powerRepository.lastWakeReason).isNull()
+ }
+
+ @Test
+ fun testSingleTap_notDreaming_noFalsingCheck() =
+ testScope.runTest {
+ // GIVEN device not dreaming with lockscreen hosted dream
+ whenever(statusBarStateController.isDreaming).thenReturn(false)
+ keyguardRepository.setIsActiveDreamLockscreenHosted(false)
+ testScope.runCurrent()
+
+ // WHEN there's a tap
+ underTest.onSingleTapUp(upEv)
+
+ // THEN the falsing manager never gets a call
+ verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt())
+ }
+}
+
+private val upEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 9188293dc751..202c7bf0d5fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -108,7 +108,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
import com.android.systemui.model.SysUiState;
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.plugins.ActivityStarter;
@@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
@Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel;
@Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor;
- @Mock protected MultiShadeInteractor mMultiShadeInteractor;
@Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel;
@Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor;
@Mock protected MotionEvent mDownMotionEvent;
@@ -370,7 +368,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mScreenOffAnimationController,
mKeyguardLogger,
mFeatureFlags,
- mInteractionJankMonitor));
+ mInteractionJankMonitor,
+ mDumpManager));
when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false);
when(mHeadsUpCallback.getContext()).thenReturn(mContext);
@@ -614,7 +613,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mLockscreenToOccludedTransitionViewModel,
mMainDispatcher,
mKeyguardTransitionInteractor,
- () -> mMultiShadeInteractor,
mDumpManager,
mKeyuardLongPressViewModel,
mKeyguardInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 0c046e93ee20..c68095ca65a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -16,17 +16,23 @@
package com.android.systemui.shade
+import android.os.VibrationEffect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
+import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewStub
import androidx.test.filters.SmallTest
import com.android.internal.util.CollectionUtils
import com.android.keyguard.KeyguardClockSwitch.LARGE
import com.android.systemui.R
+import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
@@ -55,6 +61,9 @@ class NotificationPanelViewControllerWithCoroutinesTest :
override fun getMainDispatcher() = Dispatchers.Main.immediate
+ private val ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
+ VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false)
+
@Test
fun testDisableUserSwitcherAfterEnabling_returnsViewStubToTheViewHierarchy() = runTest {
launch(Dispatchers.Main.immediate) { givenViewAttached() }
@@ -148,6 +157,43 @@ class NotificationPanelViewControllerWithCoroutinesTest :
}
@Test
+ fun doubleTapRequired_onKeyguard_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(false)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(KEYGUARD)
+
+ listener.onAdditionalTapRequired()
+ val packageName = mView.context.packageName
+ verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+ verify(mVibratorHelper)
+ .vibrate(
+ any(),
+ eq(packageName),
+ eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
+ eq("falsing-additional-tap-required"),
+ eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
+ )
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun doubleTapRequired_onKeyguard_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(KEYGUARD)
+
+ listener.onAdditionalTapRequired()
+ verify(mKeyguardIndicationController).showTransientIndication(anyInt())
+ verify(mVibratorHelper)
+ .performHapticFeedback(eq(mView), eq(HapticFeedbackConstants.REJECT))
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
fun testDoubleTapRequired_ShadeLocked() = runTest {
launch(Dispatchers.Main.immediate) {
val listener = getFalsingTapListener()
@@ -161,6 +207,45 @@ class NotificationPanelViewControllerWithCoroutinesTest :
}
@Test
+ fun doubleTapRequired_shadeLocked_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(false)
+ val listener = getFalsingTapListener()
+ val packageName = mView.context.packageName
+ mStatusBarStateController.setState(SHADE_LOCKED)
+
+ listener.onAdditionalTapRequired()
+ verify(mVibratorHelper)
+ .vibrate(
+ any(),
+ eq(packageName),
+ eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
+ eq("falsing-additional-tap-required"),
+ eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
+ )
+
+ verify(mTapAgainViewController).show()
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
+ fun doubleTapRequired_shadeLocked_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
+ launch(Dispatchers.Main.immediate) {
+ whenever(mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)).thenReturn(true)
+ val listener = getFalsingTapListener()
+ mStatusBarStateController.setState(SHADE_LOCKED)
+
+ listener.onAdditionalTapRequired()
+ verify(mVibratorHelper)
+ .performHapticFeedback(eq(mView), eq(HapticFeedbackConstants.REJECT))
+
+ verify(mTapAgainViewController).show()
+ }
+ advanceUntilIdle()
+ }
+
+ @Test
fun testOnAttachRefreshStatusBarState() = runTest {
launch(Dispatchers.Main.immediate) {
mStatusBarStateController.setState(KEYGUARD)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 5fb3a7955b5c..893123d57c99 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -34,21 +34,15 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.log.BouncerLogger
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -67,6 +61,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
@@ -80,9 +75,8 @@ import org.mockito.Mockito.anyFloat
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import java.util.Optional
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -111,12 +105,15 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
@Mock private lateinit var lockIconViewController: LockIconViewController
@Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
@Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+ @Mock
+ private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
@Mock private lateinit var notificationInsetsController: NotificationInsetsController
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
- @Mock private lateinit var unfoldTransitionProgressProvider:
- Optional<UnfoldTransitionProgressProvider>
+ @Mock
+ private lateinit var unfoldTransitionProgressProvider:
+ Optional<UnfoldTransitionProgressProvider>
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
@@ -144,22 +141,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
- featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+ featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
- val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
- val multiShadeInteractor =
- MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository =
- MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- ),
- inputProxy = inputProxy,
- )
underTest =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -183,31 +169,21 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
notificationInsetsController,
ambientState,
pulsingGestureListener,
+ mLockscreenHostedDreamGestureListener,
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
mock(KeyguardMessageAreaController.Factory::class.java),
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
- { multiShadeInteractor },
FakeSystemClock(),
- {
- MultiShadeMotionEventInteractor(
- applicationContext = context,
- applicationScope = testScope.backgroundScope,
- multiShadeInteractor = multiShadeInteractor,
- featureFlags = featureFlags,
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- ).keyguardTransitionInteractor,
- falsingManager = FalsingManagerFake(),
- shadeController = shadeController,
- )
- },
- BouncerMessageInteractor(FakeBouncerMessageRepository(),
- mock(BouncerMessageFactory::class.java),
- FakeUserRepository(), CountDownTimerUtil(), featureFlags),
+ BouncerMessageInteractor(
+ FakeBouncerMessageRepository(),
+ mock(BouncerMessageFactory::class.java),
+ FakeUserRepository(),
+ CountDownTimerUtil(),
+ featureFlags
+ ),
BouncerLogger(logcatLogBuffer("BouncerLog"))
)
underTest.setupExpandedStatusBar()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 544137e95779..ed4ac35c7272 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -34,20 +34,14 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.classifier.FalsingCollectorFake
-import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.log.BouncerLogger
-import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
-import com.android.systemui.multishade.data.repository.MultiShadeRepository
-import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
-import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.DragDownHelper
@@ -70,7 +64,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -85,7 +78,6 @@ import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -113,6 +105,8 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
@Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
@Mock private lateinit var ambientState: AmbientState
@Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+ @Mock
+ private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
@Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
@Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@@ -158,21 +152,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
val featureFlags = FakeFeatureFlags()
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
- featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
- val inputProxy = MultiShadeInputProxy()
+ featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false)
testScope = TestScope()
- val multiShadeInteractor =
- MultiShadeInteractor(
- applicationScope = testScope.backgroundScope,
- repository =
- MultiShadeRepository(
- applicationContext = context,
- inputProxy = inputProxy,
- ),
- inputProxy = inputProxy,
- )
controller =
NotificationShadeWindowViewController(
lockscreenShadeTransitionController,
@@ -196,29 +179,14 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
notificationInsetsController,
ambientState,
pulsingGestureListener,
+ mLockscreenHostedDreamGestureListener,
keyguardBouncerViewModel,
keyguardBouncerComponentFactory,
Mockito.mock(KeyguardMessageAreaController.Factory::class.java),
keyguardTransitionInteractor,
primaryBouncerToGoneTransitionViewModel,
featureFlags,
- { multiShadeInteractor },
FakeSystemClock(),
- {
- MultiShadeMotionEventInteractor(
- applicationContext = context,
- applicationScope = testScope.backgroundScope,
- multiShadeInteractor = multiShadeInteractor,
- featureFlags = featureFlags,
- keyguardTransitionInteractor =
- KeyguardTransitionInteractorFactory.create(
- scope = TestScope().backgroundScope,
- )
- .keyguardTransitionInteractor,
- falsingManager = FalsingManagerFake(),
- shadeController = shadeController,
- )
- },
BouncerMessageInteractor(
FakeBouncerMessageRepository(),
Mockito.mock(BouncerMessageFactory::class.java),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 77a22ac9b092..29bc64e6249d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -29,6 +29,7 @@ import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dock.DockManager
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.DozeInteractor
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.power.data.repository.FakePowerRepository
@@ -73,6 +74,8 @@ class PulsingGestureListenerTest : SysuiTestCase() {
@Mock
private lateinit var userTracker: UserTracker
@Mock
+ private lateinit var dozeInteractor: DozeInteractor
+ @Mock
private lateinit var screenOffAnimationController: ScreenOffAnimationController
private lateinit var powerRepository: FakePowerRepository
@@ -98,6 +101,7 @@ class PulsingGestureListenerTest : SysuiTestCase() {
ambientDisplayConfiguration,
statusBarStateController,
shadeLogger,
+ dozeInteractor,
userTracker,
tunerService,
dumpManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 1643e174ee13..b04d5d3d44e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -517,6 +517,21 @@ public class CommandQueueTest extends SysuiTestCase {
}
@Test
+ public void testConfirmImmersivePrompt() {
+ mCommandQueue.confirmImmersivePrompt();
+ waitForIdleSync();
+ verify(mCallbacks).confirmImmersivePrompt();
+ }
+
+ @Test
+ public void testImmersiveModeChanged() {
+ final int displayAreaId = 10;
+ mCommandQueue.immersiveModeChanged(displayAreaId, true);
+ waitForIdleSync();
+ verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+ }
+
+ @Test
public void testShowRearDisplayDialog() {
final int currentBaseState = 1;
mCommandQueue.showRearDisplayDialog(currentBaseState);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
index ad908e7f8000..aab4bc361d7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
@@ -6,6 +6,8 @@ import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.Vibrator
import android.testing.AndroidTestingRunner
+import android.view.HapticFeedbackConstants
+import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.eq
@@ -33,6 +35,7 @@ class VibratorHelperTest : SysuiTestCase() {
@Mock lateinit var vibrator: Vibrator
@Mock lateinit var executor: Executor
+ @Mock lateinit var view: View
@Captor lateinit var backgroundTaskCaptor: ArgumentCaptor<Runnable>
lateinit var vibratorHelper: VibratorHelper
@@ -72,6 +75,21 @@ class VibratorHelperTest : SysuiTestCase() {
}
@Test
+ fun testPerformHapticFeedback() {
+ val constant = HapticFeedbackConstants.CONFIRM
+ vibratorHelper.performHapticFeedback(view, constant)
+ verify(view).performHapticFeedback(eq(constant))
+ }
+
+ @Test
+ fun testPerformHapticFeedback_withFlags() {
+ val constant = HapticFeedbackConstants.CONFIRM
+ val flag = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
+ vibratorHelper.performHapticFeedback(view, constant, flag)
+ verify(view).performHapticFeedback(eq(constant), eq(flag))
+ }
+
+ @Test
fun testHasVibrator() {
assertThat(vibratorHelper.hasVibrator()).isTrue()
verify(vibrator).hasVibrator()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 55b6be9679f2..0b2da8bfa649 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.events
import android.content.Context
import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
import android.util.Pair
import android.view.Gravity
import android.view.View
@@ -37,11 +39,14 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
private lateinit var controller: SystemEventChipAnimationController
@@ -159,7 +164,7 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {
assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
}
- class TestView(context: Context) : View(context), BackgroundAnimatableView {
+ private class TestView(context: Context) : View(context), BackgroundAnimatableView {
override val view: View
get() = this
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
index ed249471ed27..f91e5a8cf626 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
@@ -21,7 +21,6 @@ import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
@@ -47,14 +46,11 @@ class DismissibilityCoordinatorTest : SysuiTestCase() {
private lateinit var onBeforeRenderListListener: OnBeforeRenderListListener
private val keyguardStateController: KeyguardStateController = mock()
private val pipeline: NotifPipeline = mock()
- private val flags: NotifPipelineFlags = mock()
private val dumpManager: DumpManager = mock()
@Before
fun setUp() {
- whenever(flags.allowDismissOngoing()).thenReturn(true)
-
- dismissibilityProvider = NotificationDismissibilityProviderImpl(flags, dumpManager)
+ dismissibilityProvider = NotificationDismissibilityProviderImpl(dumpManager)
coordinator = DismissibilityCoordinator(keyguardStateController, dismissibilityProvider)
coordinator.attach(pipeline)
onBeforeRenderListListener = withArgCaptor {
@@ -309,57 +305,4 @@ class DismissibilityCoordinatorTest : SysuiTestCase() {
dismissibilityProvider.isDismissable(summary)
)
}
-
- @Test
- fun testFeatureToggleOffNonDismissibleEntry() {
- whenever(flags.allowDismissOngoing()).thenReturn(false)
- val entry =
- NotificationEntryBuilder()
- .setTag("entry")
- .setFlag(mContext, Notification.FLAG_NO_DISMISS, true)
- .build()
-
- onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
- assertTrue(
- "FLAG_NO_DISMISS should be ignored, if the feature is off",
- dismissibilityProvider.isDismissable(entry)
- )
- }
-
- @Test
- fun testFeatureToggleOffOngoingNotifWhenPhoneIsLocked() {
- whenever(flags.allowDismissOngoing()).thenReturn(false)
- whenever(keyguardStateController.isUnlocked).thenReturn(false)
- val entry =
- NotificationEntryBuilder()
- .setTag("entry")
- .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
- .build()
-
- onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
- assertFalse(
- "Ongoing Notifs should NOT be dismissible, if the feature is off",
- dismissibilityProvider.isDismissable(entry)
- )
- }
-
- @Test
- fun testFeatureToggleOffOngoingNotifWhenPhoneIsUnLocked() {
- whenever(flags.allowDismissOngoing()).thenReturn(false)
- whenever(keyguardStateController.isUnlocked).thenReturn(true)
- val entry =
- NotificationEntryBuilder()
- .setTag("entry")
- .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
- .build()
-
- onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
- assertFalse(
- "Ongoing Notifs should NOT be dismissible, if the feature is off",
- dismissibilityProvider.isDismissable(entry)
- )
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
new file mode 100644
index 000000000000..4a94dc819a9e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+class GroupExpansionManagerTest : SysuiTestCase() {
+ private lateinit var gem: GroupExpansionManagerImpl
+
+ private val dumpManager: DumpManager = mock()
+ private val groupMembershipManager: GroupMembershipManager = mock()
+ private val featureFlags = FakeFeatureFlags()
+
+ private val entry1 = NotificationEntryBuilder().build()
+ private val entry2 = NotificationEntryBuilder().build()
+
+ @Before
+ fun setUp() {
+ whenever(groupMembershipManager.getGroupSummary(entry1)).thenReturn(entry1)
+ whenever(groupMembershipManager.getGroupSummary(entry2)).thenReturn(entry2)
+
+ gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags)
+ }
+
+ @Test
+ fun testNotifyOnlyOnChange_enabled() {
+ featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
+
+ var listenerCalledCount = 0
+ gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ gem.setGroupExpanded(entry1, false)
+ Assert.assertEquals(0, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(1, listenerCalledCount)
+ gem.setGroupExpanded(entry2, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry2, false)
+ Assert.assertEquals(3, listenerCalledCount)
+ }
+
+ @Test
+ fun testNotifyOnlyOnChange_disabled() {
+ featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
+
+ var listenerCalledCount = 0
+ gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+
+ gem.setGroupExpanded(entry1, false)
+ Assert.assertEquals(1, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(2, listenerCalledCount)
+ gem.setGroupExpanded(entry2, true)
+ Assert.assertEquals(3, listenerCalledCount)
+ gem.setGroupExpanded(entry1, true)
+ Assert.assertEquals(4, listenerCalledCount)
+ gem.setGroupExpanded(entry2, false)
+ Assert.assertEquals(5, listenerCalledCount)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
index 2e68cec1fe63..4d4d319a3540 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt
@@ -17,6 +17,10 @@
package com.android.systemui.statusbar.notification.row
+import android.app.Notification
+import android.net.Uri
+import android.os.UserHandle
+import android.os.UserHandle.USER_ALL
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
@@ -28,13 +32,17 @@ import com.android.systemui.flags.FeatureFlags
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.PluginManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider
import com.android.systemui.statusbar.notification.collection.render.FakeNodeController
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
@@ -45,9 +53,9 @@ import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.SystemClock
import com.android.systemui.wmshell.BubblesManager
-import java.util.Optional
import junit.framework.Assert
import org.junit.After
import org.junit.Before
@@ -55,7 +63,10 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import java.util.*
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -92,10 +103,10 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
private val featureFlags: FeatureFlags = mock()
private val peopleNotificationIdentifier: PeopleNotificationIdentifier = mock()
private val bubblesManager: BubblesManager = mock()
+ private val settingsController: NotificationSettingsController = mock()
private val dragController: ExpandableNotificationRowDragController = mock()
private val dismissibilityProvider: NotificationDismissibilityProvider = mock()
private val statusBarService: IStatusBarService = mock()
-
private lateinit var controller: ExpandableNotificationRowController
@Before
@@ -132,11 +143,16 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
featureFlags,
peopleNotificationIdentifier,
Optional.of(bubblesManager),
+ settingsController,
dragController,
dismissibilityProvider,
statusBarService
)
whenever(view.childrenContainer).thenReturn(childrenContainer)
+
+ val notification = Notification.Builder(mContext).build()
+ val sbn = SbnBuilder().setNotification(notification).build()
+ whenever(view.entry).thenReturn(NotificationEntryBuilder().setSbn(sbn).build())
}
@After
@@ -204,4 +220,74 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() {
Mockito.verify(view).removeChildNotification(eq(childView))
Mockito.verify(listContainer).notifyGroupChildRemoved(eq(childView), eq(childrenContainer))
}
+
+ @Test
+ fun registerSettingsListener_forBubbles() {
+ controller.init(mock(NotificationEntry::class.java))
+ val viewStateObserver = withArgCaptor {
+ verify(view).addOnAttachStateChangeListener(capture());
+ }
+ viewStateObserver.onViewAttachedToWindow(view);
+ verify(settingsController).addCallback(any(), any());
+ }
+
+ @Test
+ fun unregisterSettingsListener_forBubbles() {
+ controller.init(mock(NotificationEntry::class.java))
+ val viewStateObserver = withArgCaptor {
+ verify(view).addOnAttachStateChangeListener(capture());
+ }
+ viewStateObserver.onViewDetachedFromWindow(view);
+ verify(settingsController).removeCallback(any(), any());
+ }
+
+ @Test
+ fun settingsListener_invalidUri() {
+ controller.mSettingsListener.onSettingChanged(Uri.EMPTY, view.entry.sbn.userId, "1")
+
+ verify(view, never()).getPrivateLayout()
+ }
+
+ @Test
+ fun settingsListener_invalidUserId() {
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, "1")
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, null)
+
+ verify(view, never()).getPrivateLayout()
+ }
+
+ @Test
+ fun settingsListener_validUserId() {
+ val childView: NotificationContentView = mock()
+ whenever(view.privateLayout).thenReturn(childView)
+
+ controller.mSettingsListener.onSettingChanged(
+ BUBBLES_SETTING_URI, view.entry.sbn.userId, "1")
+ verify(childView).setBubblesEnabledForUser(true)
+
+ controller.mSettingsListener.onSettingChanged(
+ BUBBLES_SETTING_URI, view.entry.sbn.userId, "9")
+ verify(childView).setBubblesEnabledForUser(false)
+ }
+
+ @Test
+ fun settingsListener_userAll() {
+ val childView: NotificationContentView = mock()
+ whenever(view.privateLayout).thenReturn(childView)
+
+ val notification = Notification.Builder(mContext).build()
+ val sbn = SbnBuilder().setNotification(notification)
+ .setUser(UserHandle.of(USER_ALL))
+ .build()
+ whenever(view.entry).thenReturn(NotificationEntryBuilder()
+ .setSbn(sbn)
+ .setUser(UserHandle.of(USER_ALL))
+ .build())
+
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 9, "1")
+ verify(childView).setBubblesEnabledForUser(true)
+
+ controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 1, "0")
+ verify(childView).setBubblesEnabledForUser(false)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 608778e05dad..1dc8453a90ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -27,6 +27,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -87,6 +88,7 @@ import java.util.function.Consumer;
@RunWithLooper
public class ExpandableNotificationRowTest extends SysuiTestCase {
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private NotificationTestHelper mNotificationTestHelper;
@Rule public MockitoRule mockito = MockitoJUnit.rule();
@@ -96,12 +98,10 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
mNotificationTestHelper = new NotificationTestHelper(
mContext,
mDependency,
- TestableLooper.get(this));
+ TestableLooper.get(this),
+ mFeatureFlags);
mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
-
- FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
- fakeFeatureFlags.set(Flags.SENSITIVE_REVEAL_ANIM, false);
- mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags);
+ mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM);
}
@Test
@@ -183,6 +183,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
+ public void testSetSensitiveOnNotifRowNotifiesOfHeightChange_withOtherFlagValue()
+ throws Exception {
+ FakeFeatureFlags flags = mFeatureFlags;
+ flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+ testSetSensitiveOnNotifRowNotifiesOfHeightChange();
+ }
+
+ @Test
public void testSetSensitiveOnNotifRowNotifiesOfHeightChange() throws Exception {
// GIVEN a sensitive notification row that's currently redacted
ExpandableNotificationRow row = mNotificationTestHelper.createRow();
@@ -199,10 +207,19 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
// WHEN the row is set to no longer be sensitive
row.setSensitive(false, true);
+ boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
// VERIFY that the height change listener is invoked
assertThat(row.getShowingLayout()).isSameInstanceAs(row.getPrivateLayout());
assertThat(row.getIntrinsicHeight()).isGreaterThan(0);
- verify(listener).onHeightChanged(eq(row), eq(false));
+ verify(listener).onHeightChanged(eq(row), eq(expectAnimation));
+ }
+
+ @Test
+ public void testSetSensitiveOnGroupRowNotifiesOfHeightChange_withOtherFlagValue()
+ throws Exception {
+ FakeFeatureFlags flags = mFeatureFlags;
+ flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+ testSetSensitiveOnGroupRowNotifiesOfHeightChange();
}
@Test
@@ -222,10 +239,19 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
// WHEN the row is set to no longer be sensitive
group.setSensitive(false, true);
+ boolean expectAnimation = mFeatureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM);
// VERIFY that the height change listener is invoked
assertThat(group.getShowingLayout()).isSameInstanceAs(group.getPrivateLayout());
assertThat(group.getIntrinsicHeight()).isGreaterThan(0);
- verify(listener).onHeightChanged(eq(group), eq(false));
+ verify(listener).onHeightChanged(eq(group), eq(expectAnimation));
+ }
+
+ @Test
+ public void testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange_withOtherFlagValue()
+ throws Exception {
+ FakeFeatureFlags flags = mFeatureFlags;
+ flags.set(Flags.SENSITIVE_REVEAL_ANIM, !flags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM));
+ testSetSensitiveOnPublicRowDoesNotNotifyOfHeightChange();
}
@Test
@@ -254,7 +280,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
assertThat(publicRow.getIntrinsicHeight()).isGreaterThan(0);
assertThat(publicRow.getPrivateLayout().getMinHeight())
.isEqualTo(publicRow.getPublicLayout().getMinHeight());
- verify(listener, never()).onHeightChanged(eq(publicRow), eq(false));
+ verify(listener, never()).onHeightChanged(eq(publicRow), anyBoolean());
}
private void measureAndLayout(ExpandableNotificationRow row) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index 0b90ebec3ec6..ba6c7fd50bc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -250,6 +250,9 @@ class NotificationContentViewTest : SysuiTestCase() {
.thenReturn(actionListMarginTarget)
view.setContainingNotification(mockContainingNotification)
+ // Given: controller says bubbles are enabled for the user
+ view.setBubblesEnabledForUser(true);
+
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -301,6 +304,9 @@ class NotificationContentViewTest : SysuiTestCase() {
view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+ // Given: controller says bubbles are enabled for the user
+ view.setBubblesEnabledForUser(true);
+
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should show bubble button
view.onNotificationUpdated(createMockNotificationEntry(true))
@@ -405,7 +411,6 @@ class NotificationContentViewTest : SysuiTestCase() {
val userMock: UserHandle = mock()
whenever(this.sbn).thenReturn(sbnMock)
whenever(sbnMock.user).thenReturn(userMock)
- doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
}
private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt
new file mode 100644
index 000000000000..2bccdcafbb6e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.ActivityManager
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.provider.Settings.Secure
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.notification.row.NotificationSettingsController.Listener
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.SecureSettings
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationSettingsControllerTest : SysuiTestCase() {
+
+ val setting1: String = Secure.NOTIFICATION_BUBBLES
+ val setting2: String = Secure.ACCESSIBILITY_ENABLED
+ val settingUri1: Uri = Secure.getUriFor(setting1)
+ val settingUri2: Uri = Secure.getUriFor(setting2)
+
+ @Mock
+ private lateinit var userTracker: UserTracker
+ private lateinit var handler: Handler
+ private lateinit var testableLooper: TestableLooper
+ @Mock
+ private lateinit var secureSettings: SecureSettings
+ @Mock
+ private lateinit var dumpManager: DumpManager
+
+ @Captor
+ private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
+ @Captor
+ private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>
+
+ private lateinit var controller: NotificationSettingsController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testableLooper = TestableLooper.get(this)
+ handler = Handler(testableLooper.looper)
+ allowTestableLooperAsMainThread()
+ controller =
+ NotificationSettingsController(
+ userTracker,
+ handler,
+ secureSettings,
+ dumpManager
+ )
+ }
+
+ @After
+ fun tearDown() {
+ disallowTestableLooperAsMainThread()
+ }
+
+ @Test
+ fun creationRegistersCallbacks() {
+ verify(userTracker).addCallback(any(), any())
+ verify(dumpManager).registerNormalDumpable(anyString(), eq(controller))
+ }
+ @Test
+ fun updateContentObserverRegistration_onUserChange_noSettingsListeners() {
+ verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
+ val userCallback = userTrackerCallbackCaptor.value
+ val userId = 9
+
+ // When: User is changed
+ userCallback.onUserChanged(userId, context)
+
+ // Validate: Nothing to do, since we aren't monitoring settings
+ verify(secureSettings, never()).unregisterContentObserver(any())
+ verify(secureSettings, never()).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), any(), anyInt())
+ }
+ @Test
+ fun updateContentObserverRegistration_onUserChange_withSettingsListeners() {
+ // When: someone is listening to a setting
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+
+ verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
+ val userCallback = userTrackerCallbackCaptor.value
+ val userId = 9
+
+ // Then: User is changed
+ userCallback.onUserChanged(userId, context)
+
+ // Validate: The tracker is unregistered and re-registered with the new user
+ verify(secureSettings).unregisterContentObserver(any())
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(userId))
+ }
+
+ @Test
+ fun addCallback_onlyFirstForUriRegistersObserver() {
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), any(), anyInt())
+ }
+
+ @Test
+ fun addCallback_secondUriRegistersObserver() {
+ controller.addCallback(settingUri1,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri2,
+ Mockito.mock(Listener::class.java))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri2), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), anyBoolean(), any(), anyInt())
+ }
+
+ @Test
+ fun removeCallback_lastUnregistersObserver() {
+ val listenerSetting1 : Listener = mock()
+ val listenerSetting2 : Listener = mock()
+ controller.addCallback(settingUri1, listenerSetting1)
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser()))
+
+ controller.addCallback(settingUri2, listenerSetting2)
+ verify(secureSettings).registerContentObserverForUser(
+ eq(settingUri2), anyBoolean(), any(), anyInt())
+
+ controller.removeCallback(settingUri2, listenerSetting2)
+ verify(secureSettings, never()).unregisterContentObserver(any())
+
+ controller.removeCallback(settingUri1, listenerSetting1)
+ verify(secureSettings).unregisterContentObserver(any())
+ }
+
+ @Test
+ fun addCallback_updatesCurrentValue() {
+ whenever(secureSettings.getStringForUser(
+ setting1, ActivityManager.getCurrentUser())).thenReturn("9")
+ whenever(secureSettings.getStringForUser(
+ setting2, ActivityManager.getCurrentUser())).thenReturn("5")
+
+ val listenerSetting1a : Listener = mock()
+ val listenerSetting1b : Listener = mock()
+ val listenerSetting2 : Listener = mock()
+
+ controller.addCallback(settingUri1, listenerSetting1a)
+ controller.addCallback(settingUri1, listenerSetting1b)
+ controller.addCallback(settingUri2, listenerSetting2)
+
+ testableLooper.processAllMessages()
+
+ verify(listenerSetting1a).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting2).onSettingChanged(
+ settingUri2, ActivityManager.getCurrentUser(), "5")
+ }
+
+ @Test
+ fun removeCallback_noMoreUpdates() {
+ whenever(secureSettings.getStringForUser(
+ setting1, ActivityManager.getCurrentUser())).thenReturn("9")
+
+ val listenerSetting1a : Listener = mock()
+ val listenerSetting1b : Listener = mock()
+
+ // First, register
+ controller.addCallback(settingUri1, listenerSetting1a)
+ controller.addCallback(settingUri1, listenerSetting1b)
+ testableLooper.processAllMessages()
+
+ verify(secureSettings).registerContentObserverForUser(
+ any(Uri::class.java), anyBoolean(), capture(settingsObserverCaptor), anyInt())
+ verify(listenerSetting1a).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ Mockito.clearInvocations(listenerSetting1b)
+ Mockito.clearInvocations(listenerSetting1a)
+
+ // Remove one of them
+ controller.removeCallback(settingUri1, listenerSetting1a)
+
+ // On update, only remaining listener should get the callback
+ settingsObserverCaptor.value.onChange(false, settingUri1)
+ testableLooper.processAllMessages()
+
+ verify(listenerSetting1a, never()).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ verify(listenerSetting1b).onSettingChanged(
+ settingUri1, ActivityManager.getCurrentUser(), "9")
+ }
+
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 1a644d3540b0..d21029d33d5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -48,12 +48,16 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.widget.RemoteViews;
+import androidx.annotation.NonNull;
+
import com.android.internal.logging.MetricsLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.TestableDependency;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -90,6 +94,7 @@ import com.android.systemui.wmshell.BubblesTestActivity;
import org.mockito.ArgumentCaptor;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -130,14 +135,24 @@ public class NotificationTestHelper {
private final NotificationDismissibilityProvider mDismissibilityProvider;
public final Runnable mFutureDismissalRunnable;
private @InflationFlag int mDefaultInflationFlags;
- private FeatureFlags mFeatureFlags;
+ private final FakeFeatureFlags mFeatureFlags;
public NotificationTestHelper(
Context context,
TestableDependency dependency,
TestableLooper testLooper) {
+ this(context, dependency, testLooper, new FakeFeatureFlags());
+ }
+
+ public NotificationTestHelper(
+ Context context,
+ TestableDependency dependency,
+ TestableLooper testLooper,
+ @NonNull FakeFeatureFlags featureFlags) {
mContext = context;
mTestLooper = testLooper;
+ mFeatureFlags = Objects.requireNonNull(featureFlags);
+ dependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
dependency.injectMockDependency(NotificationMediaManager.class);
dependency.injectMockDependency(NotificationShadeWindowController.class);
dependency.injectMockDependency(MediaOutputDialogFactory.class);
@@ -183,17 +198,12 @@ public class NotificationTestHelper {
mFutureDismissalRunnable = mock(Runnable.class);
when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
.thenReturn(mFutureDismissalRunnable);
- mFeatureFlags = mock(FeatureFlags.class);
}
public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) {
mDefaultInflationFlags = defaultInflationFlags;
}
- public void setFeatureFlags(FeatureFlags featureFlags) {
- mFeatureFlags = featureFlags;
- }
-
public ExpandableNotificationRowLogger getMockLogger() {
return mMockLogger;
}
@@ -527,6 +537,10 @@ public class NotificationTestHelper {
@InflationFlag int extraInflationFlags,
int importance)
throws Exception {
+ // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
+ // set, but we do not want to override an existing value that is needed by a specific test.
+ mFeatureFlags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS);
+
LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
mContext.LAYOUT_INFLATER_SERVICE);
mRow = (ExpandableNotificationRow) inflater.inflate(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
index 09382ec1945e..3d752880f423 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt
@@ -20,7 +20,6 @@ import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -42,7 +41,6 @@ class AmbientStateTest : SysuiTestCase() {
private val bypassController = StackScrollAlgorithm.BypassController { false }
private val statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
- private val featureFlags = mock<FeatureFlags>()
private lateinit var sut: AmbientState
@@ -55,8 +53,7 @@ class AmbientStateTest : SysuiTestCase() {
sectionProvider,
bypassController,
statusBarKeyguardViewManager,
- largeScreenShadeInterpolator,
- featureFlags
+ largeScreenShadeInterpolator
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index f38881c5b521..4b145d8b0dd2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -30,7 +30,6 @@ import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.StatusBarState;
@@ -65,7 +64,6 @@ public class NotificationSectionsManagerTest extends SysuiTestCase {
@Mock private SectionHeaderController mPeopleHeaderController;
@Mock private SectionHeaderController mAlertingHeaderController;
@Mock private SectionHeaderController mSilentHeaderController;
- @Mock private FeatureFlags mFeatureFlag;
private NotificationSectionsManager mSectionsManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 8d751e3b2808..1dc0ab07349b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -9,7 +9,9 @@ import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerPr
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation
+import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.StatusBarIconView
@@ -20,6 +22,7 @@ import com.android.systemui.util.mockito.mock
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,22 +37,32 @@ import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
-class NotificationShelfTest : SysuiTestCase() {
+open class NotificationShelfTest : SysuiTestCase() {
+
+ open val useShelfRefactor: Boolean = false
+ open val useSensitiveReveal: Boolean = false
+ private val flags = FakeFeatureFlags()
@Mock
private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator
@Mock
- private lateinit var flags: FeatureFlags
- @Mock
private lateinit var ambientState: AmbientState
@Mock
private lateinit var hostLayoutController: NotificationStackScrollLayoutController
+ @Mock
+ private lateinit var hostLayout: NotificationStackScrollLayout
+ @Mock
+ private lateinit var roundnessManager: NotificationRoundnessManager
private lateinit var shelf: NotificationShelf
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ mDependency.injectTestDependency(FeatureFlags::class.java, flags)
+ flags.set(Flags.NOTIFICATION_SHELF_REFACTOR, useShelfRefactor)
+ flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal)
+ flags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS)
val root = FrameLayout(context)
shelf = LayoutInflater.from(root.context)
.inflate(/* resource = */ R.layout.status_bar_notification_shelf,
@@ -57,10 +70,13 @@ class NotificationShelfTest : SysuiTestCase() {
/* attachToRoot = */false) as NotificationShelf
whenever(ambientState.largeScreenShadeInterpolator).thenReturn(largeScreenShadeInterpolator)
- whenever(ambientState.featureFlags).thenReturn(flags)
whenever(ambientState.isSmallScreen).thenReturn(true)
- shelf.bind(ambientState, /* hostLayoutController */ hostLayoutController)
+ if (useShelfRefactor) {
+ shelf.bind(ambientState, hostLayout, roundnessManager)
+ } else {
+ shelf.bind(ambientState, hostLayoutController)
+ }
shelf.layout(/* left */ 0, /* top */ 0, /* right */ 30, /* bottom */5)
}
@@ -345,7 +361,7 @@ class NotificationShelfTest : SysuiTestCase() {
@Test
fun updateState_withNullLastVisibleBackgroundChild_hideShelf() {
// GIVEN
- shelf.setSensitiveRevealAnimEnabled(true)
+ assumeTrue(useSensitiveReveal)
whenever(ambientState.stackY).thenReturn(100f)
whenever(ambientState.stackHeight).thenReturn(100f)
val paddingBetweenElements =
@@ -372,7 +388,7 @@ class NotificationShelfTest : SysuiTestCase() {
@Test
fun updateState_withNullFirstViewInShelf_hideShelf() {
// GIVEN
- shelf.setSensitiveRevealAnimEnabled(true)
+ assumeTrue(useSensitiveReveal)
whenever(ambientState.stackY).thenReturn(100f)
whenever(ambientState.stackHeight).thenReturn(100f)
val paddingBetweenElements =
@@ -399,7 +415,7 @@ class NotificationShelfTest : SysuiTestCase() {
@Test
fun updateState_withCollapsedShade_hideShelf() {
// GIVEN
- shelf.setSensitiveRevealAnimEnabled(true)
+ assumeTrue(useSensitiveReveal)
whenever(ambientState.stackY).thenReturn(100f)
whenever(ambientState.stackHeight).thenReturn(100f)
val paddingBetweenElements =
@@ -426,7 +442,7 @@ class NotificationShelfTest : SysuiTestCase() {
@Test
fun updateState_withHiddenSectionBeforeShelf_hideShelf() {
// GIVEN
- shelf.setSensitiveRevealAnimEnabled(true)
+ assumeTrue(useSensitiveReveal)
whenever(ambientState.stackY).thenReturn(100f)
whenever(ambientState.stackHeight).thenReturn(100f)
val paddingBetweenElements =
@@ -486,3 +502,25 @@ class NotificationShelfTest : SysuiTestCase() {
assertEquals(expectedAlpha, shelf.viewState.alpha)
}
}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationShelfWithRefactorTest : NotificationShelfTest() {
+ override val useShelfRefactor: Boolean = true
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationShelfWithSensitiveRevealTest : NotificationShelfTest() {
+ override val useSensitiveReveal: Boolean = true
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationShelfWithBothFlagsTest : NotificationShelfTest() {
+ override val useShelfRefactor: Boolean = true
+ override val useSensitiveReveal: Boolean = true
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index ee8325ec02b5..07eadf7c9bb4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -54,7 +54,6 @@ import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
@@ -119,6 +118,7 @@ import java.util.Optional;
@RunWith(AndroidTestingRunner.class)
public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock private NotificationGutsManager mNotificationGutsManager;
@Mock private NotificationsController mNotificationsController;
@Mock private NotificationVisibilityProvider mVisibilityProvider;
@@ -157,7 +157,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase {
@Mock private StackStateLogger mStackLogger;
@Mock private NotificationStackScrollLogger mLogger;
@Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
- private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock private NotificationTargetsHelper mNotificationTargetsHelper;
@Mock private SecureSettings mSecureSettings;
@Mock private NotificationIconAreaController mIconAreaController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 8ad271bef2e4..72fcdec3c44c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -68,7 +68,9 @@ import com.android.systemui.ExpandHelper;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.EmptyShadeView;
@@ -106,6 +108,7 @@ import java.util.ArrayList;
@TestableLooper.RunWithLooper
public class NotificationStackScrollLayoutTest extends SysuiTestCase {
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private NotificationStackScrollLayout mStackScroller; // Normally test this
private NotificationStackScrollLayout mStackScrollerInternal; // See explanation below
private AmbientState mAmbientState;
@@ -129,7 +132,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
@Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
- @Mock private FeatureFlags mFeatureFlags;
@Before
public void setUp() throws Exception {
@@ -143,11 +145,25 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
mNotificationSectionsManager,
mBypassController,
mStatusBarKeyguardViewManager,
- mLargeScreenShadeInterpolator,
- mFeatureFlags
+ mLargeScreenShadeInterpolator
));
+ // Register the debug flags we use
+ assertFalse(Flags.NSSL_DEBUG_LINES.getDefault());
+ assertFalse(Flags.NSSL_DEBUG_REMOVE_ANIMATION.getDefault());
+ mFeatureFlags.set(Flags.NSSL_DEBUG_LINES, false);
+ mFeatureFlags.set(Flags.NSSL_DEBUG_REMOVE_ANIMATION, false);
+
+ // Register the feature flags we use
+ // TODO: Ideally we wouldn't need to set these unless a test actually reads them,
+ // and then we would test both configurations, but currently they are all read
+ // in the constructor.
+ mFeatureFlags.setDefault(Flags.SENSITIVE_REVEAL_ANIM);
+ mFeatureFlags.setDefault(Flags.ANIMATED_NOTIFICATION_SHADE_INSETS);
+ mFeatureFlags.setDefault(Flags.NOTIFICATION_SHELF_REFACTOR);
+
// Inject dependencies before initializing the layout
+ mDependency.injectTestDependency(FeatureFlags.class, mFeatureFlags);
mDependency.injectTestDependency(SysuiStatusBarStateController.class, mBarState);
mDependency.injectMockDependency(ShadeController.class);
mDependency.injectTestDependency(
@@ -176,13 +192,18 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper,
mNotificationStackSizeCalculator);
mStackScroller = spy(mStackScrollerInternal);
- mStackScroller.setShelfController(notificationShelfController);
+ if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+ mStackScroller.setShelfController(notificationShelfController);
+ }
mStackScroller.setNotificationsController(mNotificationsController);
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
when(mStackScrollLayoutController.getNotificationRoundnessManager())
.thenReturn(mNotificationRoundnessManager);
mStackScroller.setController(mStackScrollLayoutController);
+ if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+ mStackScroller.setShelf(mNotificationShelf);
+ }
doNothing().when(mGroupExpansionManager).collapseGroups();
doNothing().when(mExpandHelper).cancelImmediately();
@@ -899,7 +920,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
@Test
public void testWindowInsetAnimationProgress_updatesBottomInset() {
int bottomImeInset = 100;
- mStackScrollerInternal.setAnimatedInsetsEnabled(true);
WindowInsets windowInsets = new WindowInsets.Builder()
.setInsets(ime(), Insets.of(0, 0, 0, bottomImeInset)).build();
ArrayList<WindowInsetsAnimation> windowInsetsAnimations = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index df65c09eb8a9..85a2bdd21073 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -47,6 +47,7 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
@@ -83,7 +84,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
private Handler mHandler;
private ExpandableNotificationRow mNotificationRow;
private Runnable mFalsingCheck;
- private FeatureFlags mFeatureFlags;
+ private final FeatureFlags mFeatureFlags = new FakeFeatureFlags();
private static final int FAKE_ROW_WIDTH = 20;
private static final int FAKE_ROW_HEIGHT = 20;
@@ -96,7 +97,6 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
mCallback = mock(NotificationSwipeHelper.NotificationCallback.class);
mListener = mock(NotificationMenuRowPlugin.OnMenuEventListener.class);
mNotificationRoundnessManager = mock(NotificationRoundnessManager.class);
- mFeatureFlags = mock(FeatureFlags.class);
mSwipeHelper = spy(new NotificationSwipeHelper(
mContext.getResources(),
ViewConfiguration.get(mContext),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
index 45725ced521c..e30947ce84bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -18,6 +18,7 @@ import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class NotificationTargetsHelperTest : SysuiTestCase() {
+ private val featureFlags = FakeFeatureFlags()
lateinit var notificationTestHelper: NotificationTestHelper
private val sectionsManager: NotificationSectionsManager = mock()
private val stackScrollLayout: NotificationStackScrollLayout = mock()
@@ -26,10 +27,10 @@ class NotificationTargetsHelperTest : SysuiTestCase() {
fun setUp() {
allowTestableLooperAsMainThread()
notificationTestHelper =
- NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ NotificationTestHelper(mContext, mDependency, TestableLooper.get(this), featureFlags)
}
- private fun notificationTargetsHelper() = NotificationTargetsHelper(FakeFeatureFlags())
+ private fun notificationTargetsHelper() = NotificationTargetsHelper(featureFlags)
@Test
fun targetsForFirstNotificationInGroup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 4c97d20c5da8..987861d3f133 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -8,7 +8,6 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
import com.android.systemui.statusbar.EmptyShadeView
import com.android.systemui.statusbar.NotificationShelf
@@ -45,7 +44,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
private val dumpManager = mock<DumpManager>()
private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
private val notificationShelf = mock<NotificationShelf>()
- private val featureFlags = mock<FeatureFlags>()
private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
}
@@ -56,7 +54,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
/* bypassController */ { false },
mStatusBarKeyguardViewManager,
largeScreenShadeInterpolator,
- featureFlags,
)
private val testableResources = mContext.getOrCreateTestableResources()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 5e0e140563cd..68f2728c9ace 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.ShadeViewController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -65,6 +66,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@Mock private lateinit var keyguardViewMediator: KeyguardViewMediator
@Mock private lateinit var shadeController: ShadeController
+ @Mock private lateinit var shadeViewController: ShadeViewController
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@Mock private lateinit var lockScreenUserManager: NotificationLockscreenUserManager
@@ -91,6 +93,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
Lazy { biometricUnlockController },
Lazy { keyguardViewMediator },
Lazy { shadeController },
+ Lazy { shadeViewController },
Lazy { statusBarKeyguardViewManager },
Lazy { notifShadeWindowController },
activityLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 89f8bdbfe05b..045a63cd44a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
import static com.google.common.truth.Truth.assertThat;
@@ -39,6 +40,8 @@ import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.testing.TestableResources;
+import android.view.HapticFeedbackConstants;
+import android.view.ViewRootImpl;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.util.LatencyTracker;
@@ -47,6 +50,7 @@ import com.android.keyguard.logging.BiometricUnlockLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -118,8 +122,11 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
private VibratorHelper mVibratorHelper;
@Mock
private BiometricUnlockLogger mLogger;
+ @Mock
+ private ViewRootImpl mViewRootImpl;
private final FakeSystemClock mSystemClock = new FakeSystemClock();
private BiometricUnlockController mBiometricUnlockController;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Before
public void setUp() {
@@ -142,11 +149,14 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
mAuthController, mStatusBarStateController,
mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
- mSystemClock
+ mSystemClock,
+ mFeatureFlags
);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mBiometricUnlockController.addListener(mBiometricUnlockEventsListener);
when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker);
+ when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(mViewRootImpl);
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
}
@Test
@@ -484,6 +494,31 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintSuccess_oldPowerButtonPress_playOneWayHaptic() {
+ // GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, last wake reason was power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+
+ // GIVEN last wake time was 500ms ago
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+ mSystemClock.advanceTime(500);
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.CONFIRM)
+ );
+ }
+
+ @Test
public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() {
// GIVEN side fingerprint enrolled, wakeup just happened
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -503,6 +538,30 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintSuccess_recentGestureWakeUp_playOnewayHaptic() {
+ //GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, wakeup just happened
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // GIVEN last wake reason was from a gesture
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_GESTURE);
+
+ // WHEN biometric fingerprint succeeds
+ givenFingerprintModeUnlockCollapsing();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT,
+ true);
+
+ // THEN vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.CONFIRM)
+ );
+ }
+
+ @Test
public void onSideFingerprintFail_alwaysPlaysHaptic() {
// GIVEN side fingerprint enrolled, last wake reason was recent power button
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -518,6 +577,26 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
}
@Test
+ public void onSideFingerprintFail_alwaysPlaysOneWayHaptic() {
+ // GIVEN oneway haptics is enabled
+ mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+ // GIVEN side fingerprint enrolled, last wake reason was recent power button
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+ when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis());
+
+ // WHEN biometric fingerprint fails
+ mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+
+ // THEN always vibrate the device
+ verify(mVibratorHelper).performHapticFeedback(
+ any(),
+ eq(HapticFeedbackConstants.REJECT)
+ );
+ }
+
+ @Test
public void onFingerprintDetect_showBouncer() {
// WHEN fingerprint detect occurs
mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT,
@@ -542,4 +621,20 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase {
when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
when(mKeyguardStateController.isShowing()).thenReturn(true);
}
+
+ private void givenDreamingLocked() {
+ when(mUpdateMonitor.isDreaming()).thenReturn(true);
+ when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
+ }
+ @Test
+ public void onSideFingerprintSuccess_dreaming_unlockNoWake() {
+ when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+ when(mWakefulnessLifecycle.getLastWakeReason())
+ .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON);
+ givenDreamingLocked();
+ mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true);
+ verify(mKeyguardViewMediator).onWakeAndUnlocking();
+ // Ensure that the power hasn't been told to wake up yet.
+ verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index 85fbef0d7bb6..52f642df195e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone
import android.app.AlarmManager
import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResourcesManager
import android.content.SharedPreferences
import android.os.UserManager
import android.telecom.TelecomManager
@@ -49,6 +50,7 @@ import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.util.RingerModeTracker
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.time.DateFormatUtil
import com.android.systemui.util.time.FakeSystemClock
@@ -67,6 +69,7 @@ import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
@@ -83,6 +86,7 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
companion object {
private const val ALARM_SLOT = "alarm"
private const val CONNECTED_DISPLAY_SLOT = "connected_display"
+ private const val MANAGED_PROFILE_SLOT = "managed_profile"
}
@Mock private lateinit var iconController: StatusBarIconController
@@ -104,6 +108,7 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
@Mock private lateinit var userManager: UserManager
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var devicePolicyManagerResources: DevicePolicyResourcesManager
@Mock private lateinit var recordingController: RecordingController
@Mock private lateinit var telecomManager: TelecomManager
@Mock private lateinit var sharedPreferences: SharedPreferences
@@ -132,6 +137,12 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
com.android.internal.R.string.status_bar_alarm_clock,
ALARM_SLOT
)
+ context.orCreateTestableResources.addOverride(
+ com.android.internal.R.string.status_bar_managed_profile,
+ MANAGED_PROFILE_SLOT
+ )
+ whenever(devicePolicyManager.resources).thenReturn(devicePolicyManagerResources)
+ whenever(devicePolicyManagerResources.getString(anyString(), any())).thenReturn("")
statusBarPolicy = createStatusBarPolicy()
}
@@ -182,6 +193,36 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() {
}
@Test
+ fun testAppTransitionFinished_doesNotShowManagedProfileIcon() {
+ whenever(userManager.isManagedProfile(anyInt())).thenReturn(false)
+ whenever(keyguardStateController.isShowing).thenReturn(false)
+
+ statusBarPolicy.appTransitionFinished(0)
+ // The above call posts to bgExecutor and then back to mainExecutor
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ executor.advanceClockToLast()
+ executor.runAllReady()
+
+ verify(iconController, never()).setIconVisibility(MANAGED_PROFILE_SLOT, true)
+ }
+
+ @Test
+ fun testAppTransitionFinished_showsManagedProfileIcon() {
+ whenever(userManager.isManagedProfile(anyInt())).thenReturn(true)
+ whenever(keyguardStateController.isShowing).thenReturn(false)
+
+ statusBarPolicy.appTransitionFinished(0)
+ // The above call posts to bgExecutor and then back to mainExecutor
+ executor.advanceClockToLast()
+ executor.runAllReady()
+ executor.advanceClockToLast()
+ executor.runAllReady()
+
+ verify(iconController).setIconVisibility(MANAGED_PROFILE_SLOT, true)
+ }
+
+ @Test
fun connectedDisplay_connected_iconShown() =
testScope.runTest {
statusBarPolicy.init()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 2d96e594592c..7de0075c45ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -27,6 +27,7 @@ import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.shade.ShadeControllerImpl
import com.android.systemui.shade.ShadeLogger
import com.android.systemui.shade.ShadeViewController
@@ -50,6 +51,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.util.Optional
+import javax.inject.Provider
@SmallTest
class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@@ -73,6 +75,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Mock
private lateinit var shadeControllerImpl: ShadeControllerImpl
@Mock
+ private lateinit var sceneInteractor: Provider<SceneInteractor>
+ @Mock
private lateinit var shadeLogger: ShadeLogger
@Mock
private lateinit var viewUtil: ViewUtil
@@ -140,8 +144,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isViewEnabled).thenReturn(false)
val returnVal = view.onTouchEvent(
MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0))
@@ -152,8 +154,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isViewEnabled).thenReturn(false)
val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
@@ -165,8 +165,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isViewEnabled).thenReturn(true)
val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0)
@@ -178,8 +176,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
@Test
fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true)
- `when`(centralSurfacesImpl.shadeViewController)
- .thenReturn(shadeViewController)
`when`(shadeViewController.isFullyCollapsed).thenReturn(true)
val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
@@ -204,6 +200,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() {
userChipViewModel,
centralSurfacesImpl,
shadeControllerImpl,
+ shadeViewController,
+ sceneInteractor,
shadeLogger,
viewUtil,
configurationController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 9157cd9e4f43..085ec27a4e6b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -15,7 +15,6 @@
package com.android.systemui.statusbar.phone;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
-import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE;
import static junit.framework.Assert.assertTrue;
@@ -39,14 +38,13 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarIconView;
-import com.android.systemui.statusbar.StatusBarMobileView;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tuner.TunerService;
@@ -57,23 +55,27 @@ import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidTestingRunner.class)
-@RunWithLooper
+@RunWithLooper(setAsMainLooper = true)
@SmallTest
public class StatusBarIconControllerTest extends LeakCheckedTest {
private MobileContextProvider mMobileContextProvider = mock(MobileContextProvider.class);
+ private MobileUiAdapter mMobileUiAdapter = mock(MobileUiAdapter.class);
+ private MobileIconsViewModel mMobileIconsViewModel = mock(MobileIconsViewModel.class);
@Before
public void setup() {
injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
// For testing, ignore context overrides
when(mMobileContextProvider.getMobileContextForSub(anyInt(), any())).thenReturn(mContext);
+ when(mMobileUiAdapter.getMobileIconsViewModel()).thenReturn(mMobileIconsViewModel);
}
@Test
public void testSetCalledOnAdd_IconManager() {
LinearLayout layout = new LinearLayout(mContext);
- TestIconManager manager = new TestIconManager(layout, mMobileContextProvider);
+ TestIconManager manager =
+ new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider);
testCallOnAdd_forManager(manager);
}
@@ -83,9 +85,8 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
TestDarkIconManager manager = new TestDarkIconManager(
layout,
StatusBarLocation.HOME,
- mock(StatusBarPipelineFlags.class),
mock(WifiUiAdapter.class),
- mock(MobileUiAdapter.class),
+ mMobileUiAdapter,
mMobileContextProvider,
mock(DarkIconDispatcher.class));
testCallOnAdd_forManager(manager);
@@ -153,15 +154,10 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
assertTrue("Expected StatusBarIconView",
(manager.getViewAt(0) instanceof StatusBarIconView));
- holder = holderForType(TYPE_MOBILE);
- manager.onIconAdded(1, "test_mobile", false, holder);
- assertTrue(manager.getViewAt(1) instanceof StatusBarMobileView);
}
private StatusBarIconHolder holderForType(int type) {
switch (type) {
- case TYPE_MOBILE:
- return StatusBarIconHolder.fromMobileIconState(mock(MobileIconState.class));
case TYPE_ICON:
default:
@@ -175,14 +171,12 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
TestDarkIconManager(
LinearLayout group,
StatusBarLocation location,
- StatusBarPipelineFlags statusBarPipelineFlags,
WifiUiAdapter wifiUiAdapter,
MobileUiAdapter mobileUiAdapter,
MobileContextProvider contextProvider,
DarkIconDispatcher darkIconDispatcher) {
super(group,
location,
- statusBarPipelineFlags,
wifiUiAdapter,
mobileUiAdapter,
contextProvider,
@@ -202,23 +196,18 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
return mock;
}
-
- @Override
- protected StatusBarMobileView addMobileIcon(int index, String slot, MobileIconState state) {
- StatusBarMobileView mock = mock(StatusBarMobileView.class);
- mGroup.addView(mock, index);
-
- return mock;
- }
}
private static class TestIconManager extends IconManager implements TestableIconManager {
- TestIconManager(ViewGroup group, MobileContextProvider contextProvider) {
+ TestIconManager(
+ ViewGroup group,
+ MobileUiAdapter adapter,
+ MobileContextProvider contextProvider
+ ) {
super(group,
StatusBarLocation.HOME,
- mock(StatusBarPipelineFlags.class),
mock(WifiUiAdapter.class),
- mock(MobileUiAdapter.class),
+ adapter,
contextProvider);
}
@@ -235,14 +224,6 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
return mock;
}
-
- @Override
- protected StatusBarMobileView addMobileIcon(int index, String slot, MobileIconState state) {
- StatusBarMobileView mock = mock(StatusBarMobileView.class);
- mGroup.addView(mock, index);
-
- return mock;
- }
}
private interface TestableIconManager {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 9c7f6190de44..33144f233a71 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -64,7 +64,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.classifier.FalsingCollectorFake;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
@@ -117,6 +117,7 @@ import java.util.Optional;
public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
private static final int DISPLAY_ID = 0;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
@Mock
private AssistManager mAssistManager;
@@ -256,7 +257,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
notificationAnimationProvider,
mock(LaunchFullScreenIntentProvider.class),
mPowerInteractor,
- mock(FeatureFlags.class),
+ mFeatureFlags,
mUserTracker
);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index 5bd6ff4e73f2..9c52788dc2eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -42,7 +42,6 @@ import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.QuickSettingsController;
import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeNotificationPresenter;
import com.android.systemui.shade.ShadeViewController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -52,7 +51,6 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
@@ -81,19 +79,15 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase {
private CommandQueue mCommandQueue;
private FakeMetricsLogger mMetricsLogger;
private final ShadeController mShadeController = mock(ShadeController.class);
- private final CentralSurfaces mCentralSurfaces = mock(CentralSurfaces.class);
private final NotificationsInteractor mNotificationsInteractor =
mock(NotificationsInteractor.class);
private final KeyguardStateController mKeyguardStateController =
mock(KeyguardStateController.class);
- private final NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class);
private final InitController mInitController = new InitController();
@Before
public void setup() {
mMetricsLogger = new FakeMetricsLogger();
- LockscreenGestureLogger lockscreenGestureLogger = new LockscreenGestureLogger(
- mMetricsLogger);
mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext));
mDependency.injectTestDependency(StatusBarStateController.class,
mock(SysuiStatusBarStateController.class));
@@ -111,8 +105,6 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase {
when(notificationShadeWindowView.getResources()).thenReturn(mContext.getResources());
ShadeViewController shadeViewController = mock(ShadeViewController.class);
- when(shadeViewController.getShadeNotificationPresenter())
- .thenReturn(mock(ShadeNotificationPresenter.class));
mStatusBarNotificationPresenter = new StatusBarNotificationPresenter(
mContext,
shadeViewController,
@@ -125,7 +117,6 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase {
mock(NotificationShadeWindowController.class),
mock(DynamicPrivacyController.class),
mKeyguardStateController,
- mCentralSurfaces,
mNotificationsInteractor,
mock(LockscreenShadeTransitionController.class),
mock(PowerInteractor.class),
@@ -135,11 +126,9 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase {
mock(NotifShadeEventSource.class),
mock(NotificationMediaManager.class),
mock(NotificationGutsManager.class),
- lockscreenGestureLogger,
mInitController,
mNotificationInterruptStateProvider,
mock(NotificationRemoteInputManager.class),
- mNotifPipelineFlags,
mock(NotificationRemoteInputManager.Callback.class),
mock(NotificationListContainer.class));
mInitController.executePostInitTasks();
@@ -211,7 +200,6 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase {
when(mKeyguardStateController.isShowing()).thenReturn(true);
when(mKeyguardStateController.isOccluded()).thenReturn(false);
- when(mCentralSurfaces.isOccluded()).thenReturn(false);
assertFalse(mInterruptSuppressor.suppressAwakeHeadsUp(entry));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
index 2e9a6909e402..e76f26d8128e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
@@ -97,9 +97,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() {
powerManager,
handler = handler
)
- controller.initialize(centralSurfaces, lightRevealScrim)
- `when`(centralSurfaces.shadeViewController).thenReturn(
- shadeViewController)
+ controller.initialize(centralSurfaces, shadeViewController, lightRevealScrim)
// Screen off does not run if the panel is expanded, so we should say it's collapsed to test
// screen off.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt
index 4aa48d6f25f1..755aaa6541ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLoggerTest.kt
@@ -54,7 +54,7 @@ class MobileViewLoggerTest : SysuiTestCase() {
@Test
fun collectionStarted_dumpHasInfo() {
val view = TextView(context)
- val viewModel = QsMobileIconViewModel(commonViewModel, flags)
+ val viewModel = QsMobileIconViewModel(commonViewModel)
underTest.logCollectionStarted(view, viewModel)
@@ -66,8 +66,8 @@ class MobileViewLoggerTest : SysuiTestCase() {
fun collectionStarted_multipleViews_dumpHasInfo() {
val view = TextView(context)
val view2 = TextView(context)
- val viewModel = QsMobileIconViewModel(commonViewModel, flags)
- val viewModel2 = KeyguardMobileIconViewModel(commonViewModel, flags)
+ val viewModel = QsMobileIconViewModel(commonViewModel)
+ val viewModel2 = KeyguardMobileIconViewModel(commonViewModel)
underTest.logCollectionStarted(view, viewModel)
underTest.logCollectionStarted(view2, viewModel2)
@@ -81,8 +81,8 @@ class MobileViewLoggerTest : SysuiTestCase() {
fun collectionStopped_dumpHasInfo() {
val view = TextView(context)
val view2 = TextView(context)
- val viewModel = QsMobileIconViewModel(commonViewModel, flags)
- val viewModel2 = KeyguardMobileIconViewModel(commonViewModel, flags)
+ val viewModel = QsMobileIconViewModel(commonViewModel)
+ val viewModel2 = KeyguardMobileIconViewModel(commonViewModel)
underTest.logCollectionStarted(view, viewModel)
underTest.logCollectionStarted(view2, viewModel2)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
index 7420db2e895e..59fc0aceec97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
@@ -235,7 +235,6 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
@Test
fun onDarkChanged_iconHasNewColor() {
- whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
val view =
ModernStatusBarMobileView.constructAndBind(
context,
@@ -257,7 +256,6 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
@Test
fun setStaticDrawableColor_iconHasNewColor() {
- whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
val view =
ModernStatusBarMobileView.constructAndBind(
context,
@@ -298,7 +296,7 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
constants,
testScope.backgroundScope,
)
- viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags)
+ viewModel = QsMobileIconViewModel(viewModelCommon)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
index d5fb5776b344..e59d90f6bb66 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
@@ -86,9 +86,9 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
testScope.backgroundScope,
)
- homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags, mock())
- qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags)
- keyguardIcon = KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags)
+ homeIcon = HomeMobileIconViewModel(commonImpl, mock())
+ qsIcon = QsMobileIconViewModel(commonImpl)
+ keyguardIcon = KeyguardMobileIconViewModel(commonImpl)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
index 5bc98e0d19af..dbaa29bb3688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt
@@ -18,7 +18,9 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
@@ -43,6 +45,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@@ -50,6 +53,8 @@ import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class WifiRepositorySwitcherTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositorySwitcher
private lateinit var realImpl: WifiRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
index 9cf08c03b5d1..206ac1d37074 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt
@@ -16,15 +16,20 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class DisabledWifiRepositoryTest : SysuiTestCase() {
private lateinit var underTest: DisabledWifiRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 7007345c175c..3cf5f5249f1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -32,7 +32,9 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback
import android.net.wifi.WifiManager.UNKNOWN_SSID
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
@@ -57,6 +59,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@@ -65,6 +68,8 @@ import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
+@RoboPilotTest
+@RunWith(AndroidJUnit4::class)
class WifiRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 0d51af2754f0..3f499359cc6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -31,7 +31,6 @@ import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -46,7 +45,6 @@ import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkMode
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel.Companion.viewModelForLocation
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -64,7 +62,6 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() {
private lateinit var testableLooper: TestableLooper
- @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock private lateinit var connectivityConstants: ConnectivityConstants
@Mock private lateinit var wifiConstants: WifiConstants
@@ -110,7 +107,6 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() {
viewModel =
viewModelForLocation(
viewModelCommon,
- statusBarPipelineFlags,
StatusBarLocation.HOME,
)
}
@@ -199,7 +195,6 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() {
@Test
fun onDarkChanged_iconHasNewColor() {
- whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
@@ -215,7 +210,6 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() {
@Test
fun setStaticDrawableColor_iconHasNewColor() {
- whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 0e303b244094..cb469ead83cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -20,7 +20,6 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -58,7 +57,6 @@ class WifiViewModelTest : SysuiTestCase() {
private lateinit var underTest: WifiViewModel
- @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock private lateinit var connectivityConstants: ConnectivityConstants
@Mock private lateinit var wifiConstants: WifiConstants
@@ -107,11 +105,9 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_allLocationViewModelsReceiveSameData() =
runBlocking(IMMEDIATE) {
- val home =
- viewModelForLocation(underTest, statusBarPipelineFlags, StatusBarLocation.HOME)
- val keyguard =
- viewModelForLocation(underTest, statusBarPipelineFlags, StatusBarLocation.KEYGUARD)
- val qs = viewModelForLocation(underTest, statusBarPipelineFlags, StatusBarLocation.QS)
+ val home = viewModelForLocation(underTest, StatusBarLocation.HOME)
+ val keyguard = viewModelForLocation(underTest, StatusBarLocation.KEYGUARD)
+ val qs = viewModelForLocation(underTest, StatusBarLocation.QS)
var latestHome: WifiIcon? = null
val jobHome = home.wifiIcon.onEach { latestHome = it }.launchIn(this)
@@ -249,11 +245,9 @@ class WifiViewModelTest : SysuiTestCase() {
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
- val home =
- viewModelForLocation(underTest, statusBarPipelineFlags, StatusBarLocation.HOME)
- val keyguard =
- viewModelForLocation(underTest, statusBarPipelineFlags, StatusBarLocation.KEYGUARD)
- val qs = viewModelForLocation(underTest, statusBarPipelineFlags, StatusBarLocation.QS)
+ val home = viewModelForLocation(underTest, StatusBarLocation.HOME)
+ val keyguard = viewModelForLocation(underTest, StatusBarLocation.KEYGUARD)
+ val qs = viewModelForLocation(underTest, StatusBarLocation.QS)
var latestHome: Boolean? = null
val jobHome = home.isActivityInViewVisible.onEach { latestHome = it }.launchIn(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 391c8ca4d286..7c285b8aa1a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -102,6 +102,8 @@ public class RemoteInputViewTest extends SysuiTestCase {
"com.android.sysuitest.dummynotificationsender";
private static final int DUMMY_MESSAGE_APP_ID = Process.LAST_APPLICATION_UID - 1;
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+
@Mock private RemoteInputController mController;
@Mock private ShortcutManager mShortcutManager;
@Mock private RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
@@ -453,8 +455,7 @@ public class RemoteInputViewTest extends SysuiTestCase {
private RemoteInputViewController bindController(
RemoteInputView view,
NotificationEntry entry) {
- FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
- fakeFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
+ mFeatureFlags.set(Flags.NOTIFICATION_INLINE_REPLY_ANIMATION, true);
RemoteInputViewControllerImpl viewController = new RemoteInputViewControllerImpl(
view,
entry,
@@ -462,7 +463,7 @@ public class RemoteInputViewTest extends SysuiTestCase {
mController,
mShortcutManager,
mUiEventLoggerFake,
- fakeFeatureFlags
+ mFeatureFlags
);
viewController.bind();
return viewController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index 813597a8b576..7f990a446aaf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -101,7 +101,6 @@ class FoldAodAnimationControllerTest : SysuiTestCase() {
whenever(viewGroup.viewTreeObserver).thenReturn(viewTreeObserver)
whenever(wakefulnessLifecycle.lastSleepReason)
.thenReturn(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD)
- whenever(centralSurfaces.shadeViewController).thenReturn(shadeViewController)
whenever(shadeFoldAnimator.startFoldToAodAnimation(any(), any(), any())).then {
val onActionStarted = it.arguments[0] as Runnable
onActionStarted.run()
@@ -124,7 +123,7 @@ class FoldAodAnimationControllerTest : SysuiTestCase() {
latencyTracker,
{ keyguardInteractor },
)
- .apply { initialize(centralSurfaces, lightRevealScrim) }
+ .apply { initialize(centralSurfaces, shadeViewController, lightRevealScrim) }
verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
index d8e418a7815c..b13cb72dc944 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
@@ -26,6 +26,7 @@ import android.view.ViewRootImpl
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.eq
+import com.android.systemui.wallpapers.data.repository.FakeWallpaperRepository
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -56,6 +57,7 @@ class WallpaperControllerTest : SysuiTestCase() {
private lateinit var viewRootImpl: ViewRootImpl
@Mock
private lateinit var windowToken: IBinder
+ private val wallpaperRepository = FakeWallpaperRepository()
@JvmField
@Rule
@@ -69,7 +71,7 @@ class WallpaperControllerTest : SysuiTestCase() {
`when`(root.windowToken).thenReturn(windowToken)
`when`(root.isAttachedToWindow).thenReturn(true)
- wallaperController = WallpaperController(wallpaperManager)
+ wallaperController = WallpaperController(wallpaperManager, wallpaperRepository)
wallaperController.rootView = root
}
@@ -90,9 +92,9 @@ class WallpaperControllerTest : SysuiTestCase() {
@Test
fun setUnfoldTransitionZoom_defaultUnfoldTransitionIsDisabled_doesNotUpdateWallpaperZoom() {
- wallaperController.onWallpaperInfoUpdated(createWallpaperInfo(
+ wallpaperRepository.wallpaperInfo.value = createWallpaperInfo(
useDefaultTransition = false
- ))
+ )
wallaperController.setUnfoldTransitionZoom(0.5f)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
index 6fc36b08250b..fe5024fdc0a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
@@ -16,9 +16,11 @@
package com.android.systemui.wallpapers.data.repository
+import android.app.WallpaperInfo
import kotlinx.coroutines.flow.MutableStateFlow
/** Fake implementation of the wallpaper repository. */
class FakeWallpaperRepository : WallpaperRepository {
+ override val wallpaperInfo = MutableStateFlow<WallpaperInfo?>(null)
override val wallpaperSupportsAmbientMode = MutableStateFlow(false)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index 132b9b4a02e1..f8b096a7579b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
@@ -65,6 +65,171 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun wallpaperInfo_nullInfo() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.wallpaperInfo)
+
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(null)
+
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_WALLPAPER_CHANGED),
+ )
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun wallpaperInfo_hasInfoFromManager() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.wallpaperInfo)
+
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(UNSUPPORTED_WP)
+
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_WALLPAPER_CHANGED),
+ )
+
+ assertThat(latest).isEqualTo(UNSUPPORTED_WP)
+ }
+
+ @Test
+ fun wallpaperInfo_initialValueIsFetched() =
+ testScope.runTest {
+ whenever(wallpaperManager.getWallpaperInfoForUser(USER_WITH_SUPPORTED_WP.id))
+ .thenReturn(SUPPORTED_WP)
+ userRepository.setUserInfos(listOf(USER_WITH_SUPPORTED_WP))
+ userRepository.setSelectedUserInfo(USER_WITH_SUPPORTED_WP)
+
+ // WHEN the repo initially starts up (underTest is lazy), then it fetches the current
+ // value for the wallpaper
+ assertThat(underTest.wallpaperInfo.value).isEqualTo(SUPPORTED_WP)
+ }
+
+ @Test
+ fun wallpaperInfo_updatesOnUserChanged() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.wallpaperInfo)
+
+ val user3 = UserInfo(/* id= */ 3, /* name= */ "user3", /* flags= */ 0)
+ val user3Wp = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(user3.id)).thenReturn(user3Wp)
+
+ val user4 = UserInfo(/* id= */ 4, /* name= */ "user4", /* flags= */ 0)
+ val user4Wp = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(user4.id)).thenReturn(user4Wp)
+
+ userRepository.setUserInfos(listOf(user3, user4))
+
+ // WHEN user3 is selected
+ userRepository.setSelectedUserInfo(user3)
+
+ // THEN user3's wallpaper is used
+ assertThat(latest).isEqualTo(user3Wp)
+
+ // WHEN the user is switched to user4
+ userRepository.setSelectedUserInfo(user4)
+
+ // THEN user4's wallpaper is used
+ assertThat(latest).isEqualTo(user4Wp)
+ }
+
+ @Test
+ fun wallpaperInfo_doesNotUpdateOnUserChanging() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.wallpaperInfo)
+
+ val user3 = UserInfo(/* id= */ 3, /* name= */ "user3", /* flags= */ 0)
+ val user3Wp = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(user3.id)).thenReturn(user3Wp)
+
+ val user4 = UserInfo(/* id= */ 4, /* name= */ "user4", /* flags= */ 0)
+ val user4Wp = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(user4.id)).thenReturn(user4Wp)
+
+ userRepository.setUserInfos(listOf(user3, user4))
+
+ // WHEN user3 is selected
+ userRepository.setSelectedUserInfo(user3)
+
+ // THEN user3's wallpaper is used
+ assertThat(latest).isEqualTo(user3Wp)
+
+ // WHEN the user has started switching to user4 but hasn't finished yet
+ userRepository.selectedUser.value =
+ SelectedUserModel(user4, SelectionStatus.SELECTION_IN_PROGRESS)
+
+ // THEN the wallpaper still matches user3
+ assertThat(latest).isEqualTo(user3Wp)
+ }
+
+ @Test
+ fun wallpaperInfo_updatesOnIntent() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.wallpaperInfo)
+
+ val wp1 = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp1)
+
+ assertThat(latest).isEqualTo(wp1)
+
+ // WHEN the info is new and a broadcast is sent
+ val wp2 = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp2)
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_WALLPAPER_CHANGED),
+ )
+
+ // THEN the flow updates
+ assertThat(latest).isEqualTo(wp2)
+ }
+
+ @Test
+ fun wallpaperInfo_wallpaperNotSupported_alwaysNull() =
+ testScope.runTest {
+ whenever(wallpaperManager.isWallpaperSupported).thenReturn(false)
+
+ val latest by collectLastValue(underTest.wallpaperInfo)
+ assertThat(latest).isNull()
+
+ // Even WHEN there *is* current wallpaper
+ val wp1 = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp1)
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_WALLPAPER_CHANGED),
+ )
+
+ // THEN the value is still null because wallpaper isn't supported
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun wallpaperInfo_deviceDoesNotSupportAmbientWallpaper_alwaysFalse() =
+ testScope.runTest {
+ context.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dozeSupportsAodWallpaper,
+ false
+ )
+
+ val latest by collectLastValue(underTest.wallpaperInfo)
+ assertThat(latest).isNull()
+
+ // Even WHEN there *is* current wallpaper
+ val wp1 = mock<WallpaperInfo>()
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(wp1)
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_WALLPAPER_CHANGED),
+ )
+
+ // THEN the value is still null because wallpaper isn't supported
+ assertThat(latest).isNull()
+ }
+
+ @Test
fun wallpaperSupportsAmbientMode_nullInfo_false() =
testScope.runTest {
val latest by collectLastValue(underTest.wallpaperSupportsAmbientMode)
@@ -190,14 +355,12 @@ class WallpaperRepositoryImplTest : SysuiTestCase() {
testScope.runTest {
val latest by collectLastValue(underTest.wallpaperSupportsAmbientMode)
- val info: WallpaperInfo = mock()
- whenever(info.supportsAmbientMode()).thenReturn(false)
- whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(info)
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(UNSUPPORTED_WP)
assertThat(latest).isFalse()
// WHEN the info now supports ambient mode and a broadcast is sent
- whenever(info.supportsAmbientMode()).thenReturn(true)
+ whenever(wallpaperManager.getWallpaperInfoForUser(any())).thenReturn(SUPPORTED_WP)
fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
context,
Intent(Intent.ACTION_WALLPAPER_CHANGED),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 4839eeba2124..2f228a8da0c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -92,7 +92,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -158,6 +158,7 @@ import com.android.wm.shell.transition.Transitions;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -173,6 +174,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Optional;
+@Ignore("b/292153259")
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -305,6 +307,7 @@ public class BubblesTest extends SysuiTestCase {
private TestableLooper mTestableLooper;
private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
+ private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private UserHandle mUser0;
@@ -423,7 +426,7 @@ public class BubblesTest extends SysuiTestCase {
mCommonNotifCollection,
mNotifPipeline,
mSysUiState,
- mock(FeatureFlags.class),
+ mFeatureFlags,
mNotifPipelineFlags,
syncExecutor);
mBubblesManager.addNotifCallback(mNotifCallback);
@@ -432,7 +435,8 @@ public class BubblesTest extends SysuiTestCase {
mNotificationTestHelper = new NotificationTestHelper(
mContext,
mDependency,
- TestableLooper.get(this));
+ TestableLooper.get(this),
+ mFeatureFlags);
mRow = mNotificationTestHelper.createBubble(mDeleteIntent);
mRow2 = mNotificationTestHelper.createBubble(mDeleteIntent);
mNonBubbleNotifRow = mNotificationTestHelper.createRow();
@@ -1844,8 +1848,7 @@ public class BubblesTest extends SysuiTestCase {
}
@Test
- public void testCreateBubbleFromOngoingNotification_OngoingDismissalEnabled() {
- when(mNotifPipelineFlags.allowDismissOngoing()).thenReturn(true);
+ public void testCreateBubbleFromOngoingNotification() {
NotificationEntry notif = new NotificationEntryBuilder()
.setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
.setCanBubble(true)
@@ -1858,8 +1861,7 @@ public class BubblesTest extends SysuiTestCase {
@Test
- public void testCreateBubbleFromNoDismissNotification_OngoingDismissalEnabled() {
- when(mNotifPipelineFlags.allowDismissOngoing()).thenReturn(true);
+ public void testCreateBubbleFromNoDismissNotification() {
NotificationEntry notif = new NotificationEntryBuilder()
.setFlag(mContext, Notification.FLAG_NO_DISMISS, true)
.setCanBubble(true)
@@ -1871,37 +1873,6 @@ public class BubblesTest extends SysuiTestCase {
}
@Test
- public void testCreateBubbleFromOngoingNotification_OngoingDismissalDisabled() {
- NotificationEntry notif = new NotificationEntryBuilder()
- .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
- .setCanBubble(true)
- .build();
-
- BubbleEntry bubble = mBubblesManager.notifToBubbleEntry(notif);
-
- assertFalse(
- "Ongoing Notifis should be dismissable, if the feature is off",
- bubble.isDismissable()
- );
- }
-
-
- @Test
- public void testCreateBubbleFromNoDismissNotification_OngoingDismissalDisabled() {
- NotificationEntry notif = new NotificationEntryBuilder()
- .setFlag(mContext, Notification.FLAG_NO_DISMISS, true)
- .setCanBubble(true)
- .build();
-
- BubbleEntry bubble = mBubblesManager.notifToBubbleEntry(notif);
-
- assertTrue(
- "FLAG_NO_DISMISS should be ignored, if the feature is off",
- bubble.isDismissable()
- );
- }
-
- @Test
public void registerBubbleBarListener_barDisabled_largeScreen_shouldBeIgnored() {
mBubbleProperties.mIsBubbleBarEnabled = false;
mPositioner.setIsLargeScreen(true);
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
index 9e026576e842..2ef1be70000f 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFacePropertyRepository.kt
@@ -15,14 +15,17 @@
*
*/
-package com.android.systemui.multishade.shared.model
+package com.android.systemui.biometrics.data.repository
-/** Enumerates all known shade IDs. */
-enum class ShadeId {
- /** ID of the shade on the left in dual shade configurations. */
- LEFT,
- /** ID of the shade on the right in dual shade configurations. */
- RIGHT,
- /** ID of the single shade in single shade configurations. */
- SINGLE,
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeFacePropertyRepository : FacePropertyRepository {
+ private val faceSensorInfo = MutableStateFlow<FaceSensorInfo?>(null)
+ override val sensorInfo: Flow<FaceSensorInfo?>
+ get() = faceSensorInfo
+
+ fun setSensorInfo(value: FaceSensorInfo?) {
+ faceSensorInfo.value = value
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
index 0c5e43809fab..2362a5241244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
@@ -20,12 +20,16 @@ import android.hardware.biometrics.SensorLocationInternal
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
+ private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val isInitialized = _isInitialized.asStateFlow()
+
private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
- override val sensorId = _sensorId.asStateFlow()
+ override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
private val _strength: MutableStateFlow<SensorStrength> =
MutableStateFlow(SensorStrength.CONVENIENCE)
@@ -33,11 +37,12 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
private val _sensorType: MutableStateFlow<FingerprintSensorType> =
MutableStateFlow(FingerprintSensorType.UNKNOWN)
- override val sensorType = _sensorType.asStateFlow()
+ override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
- override val sensorLocations = _sensorLocations.asStateFlow()
+ override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+ _sensorLocations.asStateFlow()
fun setProperties(
sensorId: Int,
@@ -49,5 +54,6 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
_strength.value = strength
_sensorType.value = sensorType
_sensorLocations.value = sensorLocations
+ _isInitialized.value = true
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index b94f816e1ca4..36fa7e65d8ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -62,6 +62,32 @@ class FakeFeatureFlags : FeatureFlags {
}
}
+ /**
+ * Set the given flag's default value if no other value has been set.
+ *
+ * REMINDER: You should always test your code with your flag in both configurations, so
+ * generally you should be setting a particular value. This method should be reserved for
+ * situations where the flag needs to be read (e.g. in the class constructor), but its
+ * value shouldn't affect the actual test cases. In those cases, it's mildly safer to use
+ * this method than to hard-code `false` or `true` because then at least if you're wrong,
+ * and the flag value *does* matter, you'll notice when the flag is flipped and tests
+ * start failing.
+ */
+ fun setDefault(flag: BooleanFlag) = booleanFlags.putIfAbsent(flag.id, flag.default)
+
+ /**
+ * Set the given flag's default value if no other value has been set.
+ *
+ * REMINDER: You should always test your code with your flag in both configurations, so
+ * generally you should be setting a particular value. This method should be reserved for
+ * situations where the flag needs to be read (e.g. in the class constructor), but its
+ * value shouldn't affect the actual test cases. In those cases, it's mildly safer to use
+ * this method than to hard-code `false` or `true` because then at least if you're wrong,
+ * and the flag value *does* matter, you'll notice when the flag is flipped and tests
+ * start failing.
+ */
+ fun setDefault(flag: SysPropBooleanFlag) = booleanFlags.putIfAbsent(flag.id, flag.default)
+
private fun notifyFlagChanged(flag: Flag<*>) {
flagListeners[flag.id]?.let { listeners ->
listeners.forEach { listener ->
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
index 4aaf3478a31d..8c98aea6a990 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBiometricSettingsRepository.kt
@@ -59,7 +59,6 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
private val _authFlags = MutableStateFlow(AuthenticationFlags(0, 0))
override val authenticationFlags: Flow<AuthenticationFlags>
get() = _authFlags
-
fun setFingerprintEnrolled(isFingerprintEnrolled: Boolean) {
_isFingerprintEnrolled.value = isFingerprintEnrolled
}
@@ -110,4 +109,8 @@ class FakeBiometricSettingsRepository : BiometricSettingsRepository {
fun setIsNonStrongBiometricAllowed(value: Boolean) {
_isNonStrongBiometricAllowed.value = value
}
+
+ fun setIsStrongBiometricAllowed(value: Boolean) {
+ _isStrongBiometricAllowed.value = value
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
index 2715aaa82253..f4c2db1b944e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt
@@ -17,8 +17,8 @@
package com.android.systemui.keyguard.data.repository
import com.android.keyguard.FaceAuthUiEvent
-import com.android.systemui.keyguard.shared.model.AuthenticationStatus
-import com.android.systemui.keyguard.shared.model.DetectionStatus
+import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FaceDetectionStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -27,18 +27,23 @@ import kotlinx.coroutines.flow.filterNotNull
class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
+ private var _wasDisabled: Boolean = false
+
+ val wasDisabled: Boolean
+ get() = _wasDisabled
+
override val isAuthenticated = MutableStateFlow(false)
override val canRunFaceAuth = MutableStateFlow(false)
- private val _authenticationStatus = MutableStateFlow<AuthenticationStatus?>(null)
- override val authenticationStatus: Flow<AuthenticationStatus> =
+ private val _authenticationStatus = MutableStateFlow<FaceAuthenticationStatus?>(null)
+ override val authenticationStatus: Flow<FaceAuthenticationStatus> =
_authenticationStatus.filterNotNull()
- fun setAuthenticationStatus(status: AuthenticationStatus) {
+ fun setAuthenticationStatus(status: FaceAuthenticationStatus) {
_authenticationStatus.value = status
}
- private val _detectionStatus = MutableStateFlow<DetectionStatus?>(null)
- override val detectionStatus: Flow<DetectionStatus>
+ private val _detectionStatus = MutableStateFlow<FaceDetectionStatus?>(null)
+ override val detectionStatus: Flow<FaceDetectionStatus>
get() = _detectionStatus.filterNotNull()
- fun setDetectionStatus(status: DetectionStatus) {
+ fun setDetectionStatus(status: FaceDetectionStatus) {
_detectionStatus.value = status
}
@@ -52,6 +57,9 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository {
override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning
override val isBypassEnabled = MutableStateFlow(false)
+ override fun lockoutFaceAuth() {
+ _wasDisabled = true
+ }
override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) {
_runningAuthRequest.value = uiEvent to fallbackToDetection
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
index 4bfd3d64c98e..38791caf5bfc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
@@ -17,32 +17,38 @@
package com.android.systemui.keyguard.data.repository
+import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
class FakeDeviceEntryFingerprintAuthRepository : DeviceEntryFingerprintAuthRepository {
private val _isLockedOut = MutableStateFlow(false)
override val isLockedOut: StateFlow<Boolean> = _isLockedOut.asStateFlow()
-
- private val _isRunning = MutableStateFlow(false)
- override val isRunning: Flow<Boolean>
- get() = _isRunning
-
- private var fpSensorType = MutableStateFlow<BiometricType?>(null)
- override val availableFpSensorType: Flow<BiometricType?>
- get() = fpSensorType
-
fun setLockedOut(lockedOut: Boolean) {
_isLockedOut.value = lockedOut
}
+ private val _isRunning = MutableStateFlow(false)
+ override val isRunning: Flow<Boolean>
+ get() = _isRunning
fun setIsRunning(value: Boolean) {
_isRunning.value = value
}
+ private var fpSensorType = MutableStateFlow<BiometricType?>(null)
+ override val availableFpSensorType: Flow<BiometricType?>
+ get() = fpSensorType
fun setAvailableFpSensorType(value: BiometricType?) {
fpSensorType.value = value
}
+
+ private var _authenticationStatus = MutableStateFlow<FingerprintAuthenticationStatus?>(null)
+ override val authenticationStatus: Flow<FingerprintAuthenticationStatus>
+ get() = _authenticationStatus.filterNotNull()
+ fun setAuthenticationStatus(status: FingerprintAuthenticationStatus) {
+ _authenticationStatus.value = status
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index b9d098fe2851..8428566270de 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -22,11 +22,14 @@ import com.android.systemui.common.shared.model.Position
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.ScreenModel
+import com.android.systemui.keyguard.shared.model.ScreenState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -56,6 +59,9 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _isDozing = MutableStateFlow(false)
override val isDozing: StateFlow<Boolean> = _isDozing
+ private val _dozeTimeTick = MutableSharedFlow<Unit>()
+ override val dozeTimeTick = _dozeTimeTick
+
private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
@@ -86,6 +92,9 @@ class FakeKeyguardRepository : KeyguardRepository {
)
override val wakefulness = _wakefulnessModel
+ private val _screenModel = MutableStateFlow(ScreenModel(ScreenState.SCREEN_OFF))
+ override val screenModel = _screenModel
+
private val _isUdfpsSupported = MutableStateFlow(false)
private val _isKeyguardGoingAway = MutableStateFlow(false)
@@ -147,6 +156,10 @@ class FakeKeyguardRepository : KeyguardRepository {
_isDozing.value = isDozing
}
+ override fun dozeTimeTick() {
+ _dozeTimeTick.tryEmit(Unit)
+ }
+
override fun setLastDozeTapToWakePosition(position: Point) {
_lastDozeTapToWakePosition.value = position
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
index 7c22604dc546..b24b95e0d3d7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.data.repository
import com.android.systemui.statusbar.LightRevealEffect
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
/** Fake implementation of [LightRevealScrimRepository] */
@@ -30,4 +31,15 @@ class FakeLightRevealScrimRepository : LightRevealScrimRepository {
fun setRevealEffect(effect: LightRevealEffect) {
_revealEffect.tryEmit(effect)
}
+
+ private val _revealAmount: MutableStateFlow<Float> = MutableStateFlow(0.0f)
+ override val revealAmount: Flow<Float> = _revealAmount
+
+ override fun startRevealAmountAnimator(reveal: Boolean) {
+ if (reveal) {
+ _revealAmount.value = 1.0f
+ } else {
+ _revealAmount.value = 0.0f
+ }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 931798130499..f39982f54441 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -38,6 +38,8 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.RemoteUserInput
+import com.android.systemui.scene.shared.model.RemoteUserInputAction
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
@@ -217,5 +219,14 @@ class SceneTestUtils(
companion object {
const val CONTAINER_1 = SceneContainerNames.SYSTEM_UI_DEFAULT
const val CONTAINER_2 = "container2"
+
+ val REMOTE_INPUT_DOWN_GESTURE =
+ listOf(
+ RemoteUserInput(10f, 10f, RemoteUserInputAction.DOWN),
+ RemoteUserInput(10f, 20f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 30f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE),
+ RemoteUserInput(10f, 40f, RemoteUserInputAction.UP),
+ )
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
index 56837e8cc7ef..03e3423a3dd5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
@@ -20,7 +20,6 @@ import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
-import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState;
import java.util.List;
@@ -65,10 +64,6 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager>
}
@Override
- public void setMobileIcons(String slot, List<MobileIconState> states) {
- }
-
- @Override
public void setNewMobileIconSubIds(List<Integer> subIds) {
}
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index ca1ab9bbfebc..315972cde76b 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -42,6 +42,7 @@ import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
+import android.app.admin.DevicePolicyManagerInternal;
import android.app.assist.ActivityId;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
@@ -94,10 +95,12 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.infra.AbstractRemoteService;
import com.android.internal.infra.GlobalWhitelistState;
+import com.android.internal.os.BackgroundThread;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
import com.android.server.contentprotection.ContentProtectionBlocklistManager;
+import com.android.server.contentprotection.ContentProtectionConsentManager;
import com.android.server.contentprotection.ContentProtectionPackageManager;
import com.android.server.contentprotection.RemoteContentProtectionService;
import com.android.server.infra.AbstractMasterSystemService;
@@ -216,6 +219,8 @@ public class ContentCaptureManagerService extends
@Nullable private final ContentProtectionBlocklistManager mContentProtectionBlocklistManager;
+ @Nullable private final ContentProtectionConsentManager mContentProtectionConsentManager;
+
public ContentCaptureManagerService(@NonNull Context context) {
super(context, new FrameworkResourcesServiceNameResolver(context,
com.android.internal.R.string.config_defaultContentCaptureService),
@@ -260,12 +265,15 @@ public class ContentCaptureManagerService extends
mContentProtectionBlocklistManager = createContentProtectionBlocklistManager();
mContentProtectionBlocklistManager.updateBlocklist(
mDevCfgContentProtectionAppsBlocklistSize);
+ mContentProtectionConsentManager = createContentProtectionConsentManager();
} else {
mContentProtectionBlocklistManager = null;
+ mContentProtectionConsentManager = null;
}
} else {
mContentProtectionServiceComponentName = null;
mContentProtectionBlocklistManager = null;
+ mContentProtectionConsentManager = null;
}
}
@@ -802,6 +810,17 @@ public class ContentCaptureManagerService extends
new ContentProtectionPackageManager(getContext()));
}
+ /** @hide */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ @NonNull
+ protected ContentProtectionConsentManager createContentProtectionConsentManager() {
+ // Same handler as used by AbstractMasterSystemService
+ return new ContentProtectionConsentManager(
+ BackgroundThread.getHandler(),
+ getContext().getContentResolver(),
+ LocalServices.getService(DevicePolicyManagerInternal.class));
+ }
+
@Nullable
private ComponentName getContentProtectionServiceComponentName() {
String flatComponentName = getContentProtectionServiceFlatComponentName();
@@ -1213,7 +1232,7 @@ public class ContentCaptureManagerService extends
isContentCaptureReceiverEnabled =
isContentCaptureReceiverEnabled(userId, packageName);
isContentProtectionReceiverEnabled =
- isContentProtectionReceiverEnabled(packageName);
+ isContentProtectionReceiverEnabled(userId, packageName);
if (!isContentCaptureReceiverEnabled) {
// Full package is not allowlisted: check individual components next
@@ -1284,13 +1303,13 @@ public class ContentCaptureManagerService extends
@Override // from GlobalWhitelistState
public boolean isWhitelisted(@UserIdInt int userId, @NonNull String packageName) {
return isContentCaptureReceiverEnabled(userId, packageName)
- || isContentProtectionReceiverEnabled(packageName);
+ || isContentProtectionReceiverEnabled(userId, packageName);
}
@Override // from GlobalWhitelistState
public boolean isWhitelisted(@UserIdInt int userId, @NonNull ComponentName componentName) {
return super.isWhitelisted(userId, componentName)
- || isContentProtectionReceiverEnabled(componentName.getPackageName());
+ || isContentProtectionReceiverEnabled(userId, componentName.getPackageName());
}
private boolean isContentCaptureReceiverEnabled(
@@ -1298,9 +1317,11 @@ public class ContentCaptureManagerService extends
return super.isWhitelisted(userId, packageName);
}
- private boolean isContentProtectionReceiverEnabled(@NonNull String packageName) {
+ private boolean isContentProtectionReceiverEnabled(
+ @UserIdInt int userId, @NonNull String packageName) {
if (mContentProtectionServiceComponentName == null
- || mContentProtectionBlocklistManager == null) {
+ || mContentProtectionBlocklistManager == null
+ || mContentProtectionConsentManager == null) {
return false;
}
synchronized (mLock) {
@@ -1308,7 +1329,8 @@ public class ContentCaptureManagerService extends
return false;
}
}
- return mContentProtectionBlocklistManager.isAllowed(packageName);
+ return mContentProtectionConsentManager.isConsentGranted(userId)
+ && mContentProtectionBlocklistManager.isAllowed(packageName);
}
}
diff --git a/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionConsentManager.java b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionConsentManager.java
new file mode 100644
index 000000000000..2eb758cd5a13
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/ContentProtectionConsentManager.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.contentprotection;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Manages consent for content protection.
+ *
+ * @hide
+ */
+public class ContentProtectionConsentManager {
+
+ private static final String TAG = "ContentProtectionConsentManager";
+
+ private static final String KEY_PACKAGE_VERIFIER_USER_CONSENT = "package_verifier_user_consent";
+
+ @NonNull private final ContentResolver mContentResolver;
+
+ @NonNull private final DevicePolicyManagerInternal mDevicePolicyManagerInternal;
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ @NonNull
+ public final ContentObserver mContentObserver;
+
+ private volatile boolean mCachedPackageVerifierConsent;
+
+ public ContentProtectionConsentManager(
+ @NonNull Handler handler,
+ @NonNull ContentResolver contentResolver,
+ @NonNull DevicePolicyManagerInternal devicePolicyManagerInternal) {
+ mContentResolver = contentResolver;
+ mDevicePolicyManagerInternal = devicePolicyManagerInternal;
+ mContentObserver = new SettingsObserver(handler);
+
+ contentResolver.registerContentObserver(
+ Settings.Global.getUriFor(KEY_PACKAGE_VERIFIER_USER_CONSENT),
+ /* notifyForDescendants= */ false,
+ mContentObserver,
+ UserHandle.USER_ALL);
+ mCachedPackageVerifierConsent = isPackageVerifierConsentGranted();
+ }
+
+ /**
+ * Returns true if all the consents are granted
+ */
+ public boolean isConsentGranted(@UserIdInt int userId) {
+ return mCachedPackageVerifierConsent && !isUserOrganizationManaged(userId);
+ }
+
+ private boolean isPackageVerifierConsentGranted() {
+ // Not always cached internally
+ return Settings.Global.getInt(
+ mContentResolver, KEY_PACKAGE_VERIFIER_USER_CONSENT, /* def= */ 0)
+ >= 1;
+ }
+
+ private boolean isUserOrganizationManaged(@UserIdInt int userId) {
+ // Cached internally
+ return mDevicePolicyManagerInternal.isUserOrganizationManaged(userId);
+ }
+
+ private final class SettingsObserver extends ContentObserver {
+
+ SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri, @UserIdInt int userId) {
+ final String property = uri.getLastPathSegment();
+ if (property == null) {
+ return;
+ }
+ switch (property) {
+ case KEY_PACKAGE_VERIFIER_USER_CONSENT:
+ mCachedPackageVerifierConsent = isPackageVerifierConsentGranted();
+ return;
+ default:
+ Slog.w(TAG, "Ignoring unexpected property: " + property);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 6fd6afed49b9..754a7ede8006 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -159,9 +159,10 @@ public class PersistentDataBlockService extends SystemService {
private int getAllowedUid() {
final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
- final int mainUserId = umInternal.getMainUserId();
+ int mainUserId = umInternal.getMainUserId();
if (mainUserId < 0) {
- return -1;
+ // If main user is not defined. Use the SYSTEM user instead.
+ mainUserId = UserHandle.USER_SYSTEM;
}
String allowedPackage = mContext.getResources()
.getString(R.string.config_persistentDataPackageName);
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index c1239d53058c..d959de33d3e9 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -256,7 +256,7 @@ import java.util.function.Predicate;
public final class ActiveServices {
private static final String TAG = TAG_WITH_CLASS_NAME ? "ActiveServices" : TAG_AM;
private static final String TAG_MU = TAG + POSTFIX_MU;
- private static final String TAG_SERVICE = TAG + POSTFIX_SERVICE;
+ static final String TAG_SERVICE = TAG + POSTFIX_SERVICE;
private static final String TAG_SERVICE_EXECUTING = TAG + POSTFIX_SERVICE_EXECUTING;
private static final boolean DEBUG_DELAYED_SERVICE = DEBUG_SERVICE;
@@ -850,8 +850,7 @@ public final class ActiveServices {
// Service.startForeground()), at that point we will consult the BFSL check and the timeout
// and make the necessary decisions.
setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, r, userId,
- backgroundStartPrivileges, false /* isBindService */,
- !fgRequired /* isStartService */);
+ backgroundStartPrivileges, false /* isBindService */);
if (!mAm.mUserController.exists(r.userId)) {
Slog.w(TAG, "Trying to start service with non-existent user! " + r.userId);
@@ -894,7 +893,7 @@ public final class ActiveServices {
if (fgRequired) {
logFgsBackgroundStart(r);
- if (r.mAllowStartForeground == REASON_DENIED && isBgFgsRestrictionEnabled(r)) {
+ if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r)) {
String msg = "startForegroundService() not allowed due to "
+ "mAllowStartForeground false: service "
+ r.shortInstanceName;
@@ -1060,7 +1059,7 @@ public final class ActiveServices {
// Use that as a shortcut if possible to avoid having to recheck all the conditions.
final boolean whileInUseAllowsUiJobScheduling =
ActivityManagerService.doesReasonCodeAllowSchedulingUserInitiatedJobs(
- r.mAllowWhileInUsePermissionInFgsReason);
+ r.getFgsAllowWIU());
r.updateAllowUiJobScheduling(whileInUseAllowsUiJobScheduling
|| mAm.canScheduleUserInitiatedJobs(callingUid, callingPid, callingPackage));
} else {
@@ -2178,12 +2177,12 @@ public final class ActiveServices {
// on a SHORT_SERVICE FGS.
// See if the app could start an FGS or not.
- r.mAllowStartForeground = REASON_DENIED;
+ r.clearFgsAllowStart();
setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
r.appInfo.uid, r.intent.getIntent(), r, r.userId,
BackgroundStartPrivileges.NONE,
- false /* isBindService */, false /* isStartService */);
- if (r.mAllowStartForeground == REASON_DENIED) {
+ false /* isBindService */);
+ if (!r.isFgsAllowedStart()) {
Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: "
+ " BFSL DENIED.");
} else {
@@ -2191,13 +2190,13 @@ public final class ActiveServices {
Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: "
+ " BFSL Allowed: "
+ PowerExemptionManager.reasonCodeToString(
- r.mAllowStartForeground));
+ r.getFgsAllowStart()));
}
}
final boolean fgsStartAllowed =
!isBgFgsRestrictionEnabledForService
- || (r.mAllowStartForeground != REASON_DENIED);
+ || r.isFgsAllowedStart();
if (fgsStartAllowed) {
if (isNewTypeShortFgs) {
@@ -2246,7 +2245,7 @@ public final class ActiveServices {
setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
r.appInfo.uid, r.intent.getIntent(), r, r.userId,
BackgroundStartPrivileges.NONE,
- false /* isBindService */, false /* isStartService */);
+ false /* isBindService */);
final String temp = "startForegroundDelayMs:" + delayMs;
if (r.mInfoAllowStartForeground != null) {
r.mInfoAllowStartForeground += "; " + temp;
@@ -2266,20 +2265,21 @@ public final class ActiveServices {
setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
r.appInfo.uid, r.intent.getIntent(), r, r.userId,
BackgroundStartPrivileges.NONE,
- false /* isBindService */, false /* isStartService */);
+ false /* isBindService */);
}
// If the foreground service is not started from TOP process, do not allow it to
// have while-in-use location/camera/microphone access.
- if (!r.mAllowWhileInUsePermissionInFgs) {
+ if (!r.isFgsAllowedWIU()) {
Slog.w(TAG,
"Foreground service started from background can not have "
+ "location/camera/microphone access: service "
+ r.shortInstanceName);
}
+ r.maybeLogFgsLogicChange();
if (!bypassBfslCheck) {
logFgsBackgroundStart(r);
- if (r.mAllowStartForeground == REASON_DENIED
+ if (!r.isFgsAllowedStart()
&& isBgFgsRestrictionEnabledForService) {
final String msg = "Service.startForeground() not allowed due to "
+ "mAllowStartForeground false: service "
@@ -2378,9 +2378,9 @@ public final class ActiveServices {
// The logging of FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER event could
// be deferred, make a copy of mAllowStartForeground and
// mAllowWhileInUsePermissionInFgs.
- r.mAllowStartForegroundAtEntering = r.mAllowStartForeground;
+ r.mAllowStartForegroundAtEntering = r.getFgsAllowStart();
r.mAllowWhileInUsePermissionInFgsAtEntering =
- r.mAllowWhileInUsePermissionInFgs;
+ r.isFgsAllowedWIU();
r.mStartForegroundCount++;
r.mFgsEnterTime = SystemClock.uptimeMillis();
if (!stopProcStatsOp) {
@@ -2558,7 +2558,7 @@ public final class ActiveServices {
policy.getForegroundServiceTypePolicyInfo(type, defaultToType);
final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy(
mAm.mContext, r.packageName, r.app.uid, r.app.getPid(),
- r.mAllowWhileInUsePermissionInFgs, policyInfo);
+ r.isFgsAllowedWIU(), policyInfo);
RuntimeException exception = null;
switch (code) {
case FGS_TYPE_POLICY_CHECK_DEPRECATED: {
@@ -3701,9 +3701,7 @@ public final class ActiveServices {
}
clientPsr.addConnection(c);
c.startAssociationIfNeeded();
- // Don't set hasAboveClient if binding to self to prevent modifyRawOomAdj() from
- // dropping the process' adjustment level.
- if (b.client != s.app && c.hasFlag(Context.BIND_ABOVE_CLIENT)) {
+ if (c.hasFlag(Context.BIND_ABOVE_CLIENT)) {
clientPsr.setHasAboveClient(true);
}
if (c.hasFlag(BIND_ALLOW_WHITELIST_MANAGEMENT)) {
@@ -3744,8 +3742,7 @@ public final class ActiveServices {
}
}
setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, s, userId,
- BackgroundStartPrivileges.NONE, true /* isBindService */,
- false /* isStartService */);
+ BackgroundStartPrivileges.NONE, true /* isBindService */);
if (s.app != null) {
ProcessServiceRecord servicePsr = s.app.mServices;
@@ -7443,54 +7440,80 @@ public final class ActiveServices {
* @param callingUid caller app's uid.
* @param intent intent to start/bind service.
* @param r the service to start.
- * @param isStartService True if it's called from Context.startService().
- * False if it's called from Context.startForegroundService() or
- * Service.startForeground().
+ * @param isBindService True if it's called from bindService().
* @return true if allow, false otherwise.
*/
private void setFgsRestrictionLocked(String callingPackage,
int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId,
- BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService,
- boolean isStartService) {
+ BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService) {
+
+ @ReasonCode int allowWIU;
+ @ReasonCode int allowStart;
+
+ // If called from bindService(), do not update the actual fields, but instead
+ // keep it in a separate set of fields.
+ if (isBindService) {
+ allowWIU = r.mAllowWIUInBindService;
+ allowStart = r.mAllowStartInBindService;
+ } else {
+ allowWIU = r.mAllowWhileInUsePermissionInFgsReasonNoBinding;
+ allowStart = r.mAllowStartForegroundNoBinding;
+ }
+
// Check DeviceConfig flag.
if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) {
- if (!r.mAllowWhileInUsePermissionInFgs) {
+ if (allowWIU == REASON_DENIED) {
// BGFGS start restrictions are disabled. We're allowing while-in-use permissions.
// Note REASON_OTHER since there's no other suitable reason.
- r.mAllowWhileInUsePermissionInFgsReason = REASON_OTHER;
+ allowWIU = REASON_OTHER;
}
- r.mAllowWhileInUsePermissionInFgs = true;
}
- if (!r.mAllowWhileInUsePermissionInFgs
- || (r.mAllowStartForeground == REASON_DENIED)) {
+ if ((allowWIU == REASON_DENIED)
+ || (allowStart == REASON_DENIED)) {
@ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked(
callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges);
// We store them to compare the old and new while-in-use logics to each other.
// (They're not used for any other purposes.)
- if (!r.mAllowWhileInUsePermissionInFgs) {
- r.mAllowWhileInUsePermissionInFgs = (allowWhileInUse != REASON_DENIED);
- r.mAllowWhileInUsePermissionInFgsReason = allowWhileInUse;
+ if (allowWIU == REASON_DENIED) {
+ allowWIU = allowWhileInUse;
}
- if (r.mAllowStartForeground == REASON_DENIED) {
- r.mAllowStartForeground = shouldAllowFgsStartForegroundWithBindingCheckLocked(
+ if (allowStart == REASON_DENIED) {
+ allowStart = shouldAllowFgsStartForegroundWithBindingCheckLocked(
allowWhileInUse, callingPackage, callingPid, callingUid, intent, r,
backgroundStartPrivileges, isBindService);
}
}
+
+ if (isBindService) {
+ r.mAllowWIUInBindService = allowWIU;
+ r.mAllowStartInBindService = allowStart;
+ } else {
+ r.mAllowWhileInUsePermissionInFgsReasonNoBinding = allowWIU;
+ r.mAllowStartForegroundNoBinding = allowStart;
+
+ // Also do a binding client check, unless called from bindService().
+ if (r.mAllowWIUByBindings == REASON_DENIED) {
+ r.mAllowWIUByBindings =
+ shouldAllowFgsWhileInUsePermissionByBindingsLocked(callingUid);
+ }
+ if (r.mAllowStartByBindings == REASON_DENIED) {
+ r.mAllowStartByBindings = r.mAllowWIUByBindings;
+ }
+ }
}
/**
* Reset various while-in-use and BFSL related information.
*/
void resetFgsRestrictionLocked(ServiceRecord r) {
- r.mAllowWhileInUsePermissionInFgs = false;
- r.mAllowWhileInUsePermissionInFgsReason = REASON_DENIED;
- r.mAllowStartForeground = REASON_DENIED;
+ r.clearFgsAllowWIU();
+ r.clearFgsAllowStart();
+
r.mInfoAllowStartForeground = null;
r.mInfoTempFgsAllowListReason = null;
r.mLoggedInfoAllowStartForeground = false;
- r.updateAllowUiJobScheduling(r.mAllowWhileInUsePermissionInFgs);
+ r.updateAllowUiJobScheduling(r.isFgsAllowedWIU());
}
boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) {
@@ -8062,10 +8085,10 @@ public final class ActiveServices {
*/
if (!r.mLoggedInfoAllowStartForeground) {
final String msg = "Background started FGS: "
- + ((r.mAllowStartForeground != REASON_DENIED) ? "Allowed " : "Disallowed ")
+ + (r.isFgsAllowedStart() ? "Allowed " : "Disallowed ")
+ r.mInfoAllowStartForeground
+ (r.isShortFgs() ? " (Called on SHORT_SERVICE)" : "");
- if (r.mAllowStartForeground != REASON_DENIED) {
+ if (r.isFgsAllowedStart()) {
if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName,
mAm.mConstants.mFgsStartAllowedLogSampleRate)) {
Slog.wtfQuiet(TAG, msg);
@@ -8105,8 +8128,8 @@ public final class ActiveServices {
allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering;
fgsStartReasonCode = r.mAllowStartForegroundAtEntering;
} else {
- allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgs;
- fgsStartReasonCode = r.mAllowStartForeground;
+ allowWhileInUsePermissionInFgs = r.isFgsAllowedWIU();
+ fgsStartReasonCode = r.getFgsAllowStart();
}
final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null
? r.mRecentCallerApplicationInfo.targetSdkVersion : 0;
@@ -8143,7 +8166,13 @@ public final class ActiveServices {
mAm.getUidStateLocked(r.mRecentCallingUid),
mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid),
0,
- 0);
+ 0,
+ r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ r.mAllowWIUInBindService,
+ r.mAllowWIUByBindings,
+ r.mAllowStartForegroundNoBinding,
+ r.mAllowStartInBindService,
+ r.mAllowStartByBindings);
int event = 0;
if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) {
@@ -8295,8 +8324,7 @@ public final class ActiveServices {
r.mFgsEnterTime = SystemClock.uptimeMillis();
r.foregroundServiceType = options.mForegroundServiceTypes;
setFgsRestrictionLocked(callingPackage, callingPid, callingUid, intent, r, userId,
- BackgroundStartPrivileges.NONE, false /* isBindService */,
- false /* isStartService */);
+ BackgroundStartPrivileges.NONE, false /* isBindService */);
final ProcessServiceRecord psr = callerApp.mServices;
final boolean newService = psr.startService(r);
// updateOomAdj.
diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
index 38e7371e7075..786e1cc7075f 100644
--- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
+++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java
@@ -479,8 +479,8 @@ public class ForegroundServiceTypeLoggerModule {
r.appInfo.uid,
r.shortInstanceName,
fgsState, // FGS State
- r.mAllowWhileInUsePermissionInFgs, // allowWhileInUsePermissionInFgs
- r.mAllowStartForeground, // fgsStartReasonCode
+ r.isFgsAllowedWIU(), // allowWhileInUsePermissionInFgs
+ r.getFgsAllowStart(), // fgsStartReasonCode
r.appInfo.targetSdkVersion,
r.mRecentCallingUid,
0, // callerTargetSdkVersion
@@ -506,7 +506,13 @@ public class ForegroundServiceTypeLoggerModule {
ActivityManager.PROCESS_STATE_UNKNOWN,
ActivityManager.PROCESS_CAPABILITY_NONE,
apiDurationBeforeFgsStart,
- apiDurationAfterFgsEnd);
+ apiDurationAfterFgsEnd,
+ r.mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ r.mAllowWIUInBindService,
+ r.mAllowWIUByBindings,
+ r.mAllowStartForegroundNoBinding,
+ r.mAllowStartInBindService,
+ r.mAllowStartByBindings);
}
/**
@@ -557,7 +563,13 @@ public class ForegroundServiceTypeLoggerModule {
ActivityManager.PROCESS_STATE_UNKNOWN,
ActivityManager.PROCESS_CAPABILITY_NONE,
0,
- apiDurationAfterFgsEnd);
+ apiDurationAfterFgsEnd,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0);
}
/**
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index a682c85f03b2..459c6ff3504a 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2222,7 +2222,7 @@ public class OomAdjuster {
if (s.isForeground) {
final int fgsType = s.foregroundServiceType;
- if (s.mAllowWhileInUsePermissionInFgs) {
+ if (s.isFgsAllowedWIU()) {
capabilityFromFGS |=
(fgsType & FOREGROUND_SERVICE_TYPE_LOCATION)
!= 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0;
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index a0e76f1d6af9..65ca5d35503a 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -42,7 +42,6 @@ import android.os.Bundle;
import android.os.IBinder;
import android.os.PowerWhitelistManager;
import android.os.PowerWhitelistManager.ReasonCode;
-import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
@@ -384,14 +383,6 @@ public final class PendingIntentRecord extends IIntentSender.Stub {
})
public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges(
int callingUid, @Nullable String callingPackage) {
- if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
- // We temporarily allow BAL for system processes, while we verify that all valid use
- // cases are opted in explicitly to grant their BAL permission.
- // Background: In many cases devices are running additional apps that share UID with
- // the system. If one of these apps targets a lower SDK the change is not active, but
- // as soon as that app is upgraded (or removed) BAL would be blocked. (b/283138430)
- return BackgroundStartPrivileges.ALLOW_BAL;
- }
boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled(
DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,
UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(
diff --git a/services/core/java/com/android/server/am/ProcessServiceRecord.java b/services/core/java/com/android/server/am/ProcessServiceRecord.java
index 7ff6d116baaf..81d0b6ac700b 100644
--- a/services/core/java/com/android/server/am/ProcessServiceRecord.java
+++ b/services/core/java/com/android/server/am/ProcessServiceRecord.java
@@ -341,8 +341,7 @@ final class ProcessServiceRecord {
mHasAboveClient = false;
for (int i = mConnections.size() - 1; i >= 0; i--) {
ConnectionRecord cr = mConnections.valueAt(i);
- if (cr.binding.service.app.mServices != this
- && cr.hasFlag(Context.BIND_ABOVE_CLIENT)) {
+ if (cr.hasFlag(Context.BIND_ABOVE_CLIENT)) {
mHasAboveClient = true;
break;
}
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 50fe6d71d26e..aabab61c36f4 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -21,9 +21,11 @@ import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_BOUND_SERVICE;
import static android.os.PowerExemptionManager.REASON_DENIED;
+import static android.os.PowerExemptionManager.reasonCodeToString;
import static android.os.Process.INVALID_UID;
import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.server.am.ActiveServices.TAG_SERVICE;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_SERVICE;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -172,11 +174,11 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
private BackgroundStartPrivileges mBackgroundStartPrivilegesByStartMerged =
BackgroundStartPrivileges.NONE;
- // allow while-in-use permissions in foreground service or not.
+ // Reason code for allow while-in-use permissions in foreground service.
+ // If it's not DENIED, while-in-use permissions are allowed.
// while-in-use permissions in FGS started from background might be restricted.
- boolean mAllowWhileInUsePermissionInFgs;
@PowerExemptionManager.ReasonCode
- int mAllowWhileInUsePermissionInFgsReason = REASON_DENIED;
+ int mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
// A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state.
boolean mAllowWhileInUsePermissionInFgsAtEntering;
@@ -205,15 +207,114 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
// allow the service becomes foreground service? Service started from background may not be
// allowed to become a foreground service.
- @PowerExemptionManager.ReasonCode int mAllowStartForeground = REASON_DENIED;
+ @PowerExemptionManager.ReasonCode
+ int mAllowStartForegroundNoBinding = REASON_DENIED;
// A copy of mAllowStartForeground's value when the service is entering FGS state.
- @PowerExemptionManager.ReasonCode int mAllowStartForegroundAtEntering = REASON_DENIED;
+ @PowerExemptionManager.ReasonCode
+ int mAllowStartForegroundAtEntering = REASON_DENIED;
// Debug info why mAllowStartForeground is allowed or denied.
String mInfoAllowStartForeground;
// Debug info if mAllowStartForeground is allowed because of a temp-allowlist.
ActivityManagerService.FgsTempAllowListItem mInfoTempFgsAllowListReason;
// Is the same mInfoAllowStartForeground string has been logged before? Used for dedup.
boolean mLoggedInfoAllowStartForeground;
+
+ @PowerExemptionManager.ReasonCode
+ int mAllowWIUInBindService = REASON_DENIED;
+
+ @PowerExemptionManager.ReasonCode
+ int mAllowWIUByBindings = REASON_DENIED;
+
+ @PowerExemptionManager.ReasonCode
+ int mAllowStartInBindService = REASON_DENIED;
+
+ @PowerExemptionManager.ReasonCode
+ int mAllowStartByBindings = REASON_DENIED;
+
+ @PowerExemptionManager.ReasonCode
+ int getFgsAllowWIU() {
+ return mAllowWhileInUsePermissionInFgsReasonNoBinding != REASON_DENIED
+ ? mAllowWhileInUsePermissionInFgsReasonNoBinding
+ : mAllowWIUInBindService;
+ }
+
+ boolean isFgsAllowedWIU() {
+ return getFgsAllowWIU() != REASON_DENIED;
+ }
+
+ @PowerExemptionManager.ReasonCode
+ int getFgsAllowStart() {
+ return mAllowStartForegroundNoBinding != REASON_DENIED
+ ? mAllowStartForegroundNoBinding
+ : mAllowStartInBindService;
+ }
+
+ boolean isFgsAllowedStart() {
+ return getFgsAllowStart() != REASON_DENIED;
+ }
+
+ void clearFgsAllowWIU() {
+ mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED;
+ mAllowWIUInBindService = REASON_DENIED;
+ mAllowWIUByBindings = REASON_DENIED;
+ }
+
+ void clearFgsAllowStart() {
+ mAllowStartForegroundNoBinding = REASON_DENIED;
+ mAllowStartInBindService = REASON_DENIED;
+ mAllowStartByBindings = REASON_DENIED;
+ }
+
+ @PowerExemptionManager.ReasonCode
+ int reasonOr(@PowerExemptionManager.ReasonCode int first,
+ @PowerExemptionManager.ReasonCode int second) {
+ return first != REASON_DENIED ? first : second;
+ }
+
+ boolean allowedChanged(@PowerExemptionManager.ReasonCode int first,
+ @PowerExemptionManager.ReasonCode int second) {
+ return (first == REASON_DENIED) != (second == REASON_DENIED);
+ }
+
+ String changeMessage(@PowerExemptionManager.ReasonCode int first,
+ @PowerExemptionManager.ReasonCode int second) {
+ return reasonOr(first, second) == REASON_DENIED ? "DENIED"
+ : ("ALLOWED ("
+ + reasonCodeToString(first)
+ + "+"
+ + reasonCodeToString(second)
+ + ")");
+ }
+
+ void maybeLogFgsLogicChange() {
+ final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ mAllowWIUInBindService);
+ final int newWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ mAllowWIUByBindings);
+ final int origStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartInBindService);
+ final int newStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+
+ final boolean wiuChanged = allowedChanged(origWiu, newWiu);
+ final boolean startChanged = allowedChanged(origStart, newStart);
+
+ if (!wiuChanged && !startChanged) {
+ return;
+ }
+ final String message = "FGS logic changed:"
+ + (wiuChanged ? " [WIU changed]" : "")
+ + (startChanged ? " [BFSL changed]" : "")
+ + " OW:" // Orig-WIU
+ + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding,
+ mAllowWIUInBindService)
+ + " NW:" // New-WIU
+ + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUByBindings)
+ + " OS:" // Orig-start
+ + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService)
+ + " NS:" // New-start
+ + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings);
+ Slog.wtf(TAG_SERVICE, message);
+ }
+
// The number of times Service.startForeground() is called, after this service record is
// created. (i.e. due to "bound" or "start".) It never decreases, even when stopForeground()
// is called.
@@ -502,7 +603,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
ProtoUtils.toDuration(proto, ServiceRecordProto.RESTART_TIME, restartTime, now);
proto.write(ServiceRecordProto.CREATED_FROM_FG, createdFromFg);
proto.write(ServiceRecordProto.ALLOW_WHILE_IN_USE_PERMISSION_IN_FGS,
- mAllowWhileInUsePermissionInFgs);
+ isFgsAllowedWIU());
if (startRequested || delayedStop || lastStartId != 0) {
long startToken = proto.start(ServiceRecordProto.START);
@@ -618,7 +719,13 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
pw.println(mBackgroundStartPrivilegesByStartMerged);
}
pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason=");
- pw.println(PowerExemptionManager.reasonCodeToString(mAllowWhileInUsePermissionInFgsReason));
+ pw.println(PowerExemptionManager.reasonCodeToString(
+ mAllowWhileInUsePermissionInFgsReasonNoBinding));
+
+ pw.print(prefix); pw.print("mAllowWIUInBindService=");
+ pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUInBindService));
+ pw.print(prefix); pw.print("mAllowWIUByBindings=");
+ pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUByBindings));
pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling);
pw.print(prefix); pw.print("recentCallingPackage=");
@@ -626,7 +733,12 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN
pw.print(prefix); pw.print("recentCallingUid=");
pw.println(mRecentCallingUid);
pw.print(prefix); pw.print("allowStartForeground=");
- pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForeground));
+ pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForegroundNoBinding));
+ pw.print(prefix); pw.print("mAllowStartInBindService=");
+ pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartInBindService));
+ pw.print(prefix); pw.print("mAllowStartByBindings=");
+ pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartByBindings));
+
pw.print(prefix); pw.print("startForegroundCount=");
pw.println(mStartForegroundCount);
pw.print(prefix); pw.print("infoAllowStartForeground=");
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
index 78c38089e803..8a54ae5a6fea 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java
@@ -580,7 +580,7 @@ public class BiometricScheduler {
}
final BiometricSchedulerOperation operation = mCurrentOperation;
mHandler.postDelayed(() -> {
- if (operation == mCurrentOperation) {
+ if (operation == mCurrentOperation && !operation.isFinished()) {
Counter.logIncrement("biometric.value_scheduler_watchdog_triggered_count");
clearScheduler();
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index 54d1faa39be0..3d0ea9d8bef6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -260,14 +260,6 @@ class FingerprintAuthenticationClient
final AidlSession session = getFreshDaemon();
final OperationContextExt opContext = getOperationContext();
- final ICancellationSignal cancel;
- if (session.hasContextMethods()) {
- cancel = session.getSession().authenticateWithContext(
- mOperationId, opContext.toAidlContext(getOptions()));
- } else {
- cancel = session.getSession().authenticate(mOperationId);
- }
-
getBiometricContext().subscribe(opContext, ctx -> {
if (session.hasContextMethods()) {
try {
@@ -289,7 +281,12 @@ class FingerprintAuthenticationClient
mALSProbeCallback.getProbe().enable();
}
- return cancel;
+ if (session.hasContextMethods()) {
+ return session.getSession().authenticateWithContext(
+ mOperationId, opContext.toAidlContext(getOptions()));
+ } else {
+ return session.getSession().authenticate(mOperationId);
+ }
}
@Override
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index 0b04159194d1..f8f0088ac047 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -613,16 +613,26 @@ public class CameraServiceProxy extends SystemService
@Override
public boolean isCameraDisabled(int userId) {
- DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
- if (dpm == null) {
- Slog.e(TAG, "Failed to get the device policy manager service");
+ if (Binder.getCallingUid() != Process.CAMERASERVER_UID) {
+ Slog.e(TAG, "Calling UID: " + Binder.getCallingUid()
+ + " doesn't match expected camera service UID!");
return false;
}
+ final long ident = Binder.clearCallingIdentity();
try {
- return dpm.getCameraDisabled(null, userId);
- } catch (Exception e) {
- e.printStackTrace();
- return false;
+ DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+ if (dpm == null) {
+ Slog.e(TAG, "Failed to get the device policy manager service");
+ return false;
+ }
+ try {
+ return dpm.getCameraDisabled(null, userId);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
};
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 5b11cfe7ff06..4bfc09075448 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -36,16 +36,18 @@ class BrightnessRangeController {
BrightnessRangeController(HighBrightnessModeController hbmController,
- Runnable modeChangeCallback) {
- this(hbmController, modeChangeCallback,
+ Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig) {
+ this(hbmController, modeChangeCallback, displayDeviceConfig,
new DeviceConfigParameterProvider(DeviceConfigInterface.REAL));
}
BrightnessRangeController(HighBrightnessModeController hbmController,
- Runnable modeChangeCallback, DeviceConfigParameterProvider configParameterProvider) {
+ Runnable modeChangeCallback, DisplayDeviceConfig displayDeviceConfig,
+ DeviceConfigParameterProvider configParameterProvider) {
mHbmController = hbmController;
mModeChangeCallback = modeChangeCallback;
mUseNbmController = configParameterProvider.isNormalBrightnessControllerFeatureEnabled();
+ mNormalBrightnessModeController.resetNbmData(displayDeviceConfig.getLuxThrottlingData());
}
void dump(PrintWriter pw) {
diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java
index dd5afa2bdc39..da51569ee5cc 100644
--- a/services/core/java/com/android/server/display/DisplayBrightnessState.java
+++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java
@@ -33,12 +33,15 @@ public final class DisplayBrightnessState {
private final String mDisplayBrightnessStrategyName;
private final boolean mShouldUseAutoBrightness;
+ private final boolean mIsSlowChange;
+
private DisplayBrightnessState(Builder builder) {
mBrightness = builder.getBrightness();
mSdrBrightness = builder.getSdrBrightness();
mBrightnessReason = builder.getBrightnessReason();
mDisplayBrightnessStrategyName = builder.getDisplayBrightnessStrategyName();
mShouldUseAutoBrightness = builder.getShouldUseAutoBrightness();
+ mIsSlowChange = builder.isSlowChange();
}
/**
@@ -77,6 +80,13 @@ public final class DisplayBrightnessState {
return mShouldUseAutoBrightness;
}
+ /**
+ * @return {@code true} if the should transit to new state slowly
+ */
+ public boolean isSlowChange() {
+ return mIsSlowChange;
+ }
+
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:");
@@ -88,6 +98,8 @@ public final class DisplayBrightnessState {
stringBuilder.append(getBrightnessReason());
stringBuilder.append("\n shouldUseAutoBrightness:");
stringBuilder.append(getShouldUseAutoBrightness());
+ stringBuilder.append("\n isSlowChange:");
+ stringBuilder.append(mIsSlowChange);
return stringBuilder.toString();
}
@@ -111,13 +123,14 @@ public final class DisplayBrightnessState {
&& mBrightnessReason.equals(otherState.getBrightnessReason())
&& TextUtils.equals(mDisplayBrightnessStrategyName,
otherState.getDisplayBrightnessStrategyName())
- && mShouldUseAutoBrightness == otherState.getShouldUseAutoBrightness();
+ && mShouldUseAutoBrightness == otherState.getShouldUseAutoBrightness()
+ && mIsSlowChange == otherState.isSlowChange();
}
@Override
public int hashCode() {
- return Objects.hash(
- mBrightness, mSdrBrightness, mBrightnessReason, mShouldUseAutoBrightness);
+ return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason,
+ mShouldUseAutoBrightness, mIsSlowChange);
}
/**
@@ -129,6 +142,7 @@ public final class DisplayBrightnessState {
private BrightnessReason mBrightnessReason = new BrightnessReason();
private String mDisplayBrightnessStrategyName;
private boolean mShouldUseAutoBrightness;
+ private boolean mIsSlowChange;
/**
* Create a builder starting with the values from the specified {@link
@@ -143,6 +157,7 @@ public final class DisplayBrightnessState {
builder.setBrightnessReason(state.getBrightnessReason());
builder.setDisplayBrightnessStrategyName(state.getDisplayBrightnessStrategyName());
builder.setShouldUseAutoBrightness(state.getShouldUseAutoBrightness());
+ builder.setIsSlowChange(state.isSlowChange());
return builder;
}
@@ -237,6 +252,21 @@ public final class DisplayBrightnessState {
}
/**
+ * See {@link DisplayBrightnessState#isSlowChange()}.
+ */
+ public Builder setIsSlowChange(boolean shouldUseAutoBrightness) {
+ this.mIsSlowChange = shouldUseAutoBrightness;
+ return this;
+ }
+
+ /**
+ * See {@link DisplayBrightnessState#isSlowChange()}.
+ */
+ public boolean isSlowChange() {
+ return mIsSlowChange;
+ }
+
+ /**
* This is used to construct an immutable DisplayBrightnessState object from its builder
*/
public DisplayBrightnessState build() {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 3b779ecf77e5..626502ef07b4 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3151,11 +3151,6 @@ public final class DisplayManagerService extends SystemService {
// with the corresponding displaydevice.
HighBrightnessModeMetadata hbmMetadata =
mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
- if (hbmMetadata == null) {
- Slog.wtf(TAG, "High Brightness Mode Metadata is null in DisplayManagerService for "
- + "display: " + display.getDisplayIdLocked());
- return null;
- }
if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) {
displayPowerController = new DisplayPowerController2(
mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index c5d0c177a46d..1dfe6b2de5ab 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -450,6 +450,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
private float[] mNitsRange;
private final BrightnessRangeController mBrightnessRangeController;
+
+ @Nullable
private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;
private final BrightnessThrottler mBrightnessThrottler;
@@ -673,7 +675,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback);
mBrightnessRangeController = new BrightnessRangeController(hbmController,
- modeChangeCallback);
+ modeChangeCallback, mDisplayDeviceConfig);
mBrightnessThrottler = createBrightnessThrottlerLocked();
@@ -2286,8 +2288,17 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
if (!reportOnly && mPowerState.getScreenState() != state
&& readyToUpdateDisplayState()) {
Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
- // TODO(b/153319140) remove when we can get this from the above trace invocation
- SystemProperties.set("debug.tracing.screen_state", String.valueOf(state));
+
+ String propertyKey = "debug.tracing.screen_state";
+ String propertyValue = String.valueOf(state);
+ try {
+ // TODO(b/153319140) remove when we can get this from the above trace invocation
+ SystemProperties.set(propertyKey, propertyValue);
+ } catch (RuntimeException e) {
+ Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
+ + " value=" + propertyValue + " " + e.getMessage());
+ }
+
mPowerState.setScreenState(state);
// Tell battery stats about the transition.
noteScreenState(state);
@@ -2380,8 +2391,17 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate)) {
Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target);
- // TODO(b/153319140) remove when we can get this from the above trace invocation
- SystemProperties.set("debug.tracing.screen_brightness", String.valueOf(target));
+
+ String propertyKey = "debug.tracing.screen_brightness";
+ String propertyValue = String.valueOf(target);
+ try {
+ // TODO(b/153319140) remove when we can get this from the above trace invocation
+ SystemProperties.set(propertyKey, propertyValue);
+ } catch (RuntimeException e) {
+ Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
+ + " value=" + propertyValue + " " + e.getMessage());
+ }
+
noteScreenBrightness(target);
}
}
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index c2c0c0a496b6..1d8b4949bd86 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -442,9 +442,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
@Nullable
private BrightnessMappingStrategy mIdleModeBrightnessMapper;
- // Indicates whether we should ramp slowly to the brightness value to follow.
- private boolean mBrightnessToFollowSlowChange;
-
private boolean mIsRbcActive;
// Animators.
@@ -553,7 +550,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
mBrightnessThrottler = createBrightnessThrottlerLocked();
mBrightnessRangeController = new BrightnessRangeController(hbmController,
- modeChangeCallback);
+ modeChangeCallback, mDisplayDeviceConfig);
mDisplayBrightnessController =
new DisplayBrightnessController(context, null,
@@ -1289,7 +1286,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
// actual state instead of the desired one.
animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
state = mPowerState.getScreenState();
- boolean slowChange = false;
+
final boolean userSetBrightnessChanged = mDisplayBrightnessController
.updateUserSetScreenBrightness();
@@ -1298,11 +1295,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
float brightnessState = displayBrightnessState.getBrightness();
float rawBrightnessState = displayBrightnessState.getBrightness();
mBrightnessReasonTemp.set(displayBrightnessState.getBrightnessReason());
-
- if (displayBrightnessState.getBrightnessReason().getReason()
- == BrightnessReason.REASON_FOLLOWER) {
- slowChange = mBrightnessToFollowSlowChange;
- }
+ boolean slowChange = displayBrightnessState.isSlowChange();
// Set up the ScreenOff controller used when coming out of SCREEN_OFF and the ALS sensor
// doesn't yet have a valid lux value to use with auto-brightness.
@@ -1352,6 +1345,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
.getRawAutomaticScreenBrightness();
brightnessState = clampScreenBrightness(brightnessState);
// slowly adapt to auto-brightness
+ // TODO(b/253226419): slowChange should be decided by strategy.updateBrightness
slowChange = mAutomaticBrightnessStrategy.hasAppliedAutoBrightness()
&& !mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged();
brightnessAdjustmentFlags =
@@ -1953,8 +1947,17 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
if (!reportOnly && mPowerState.getScreenState() != state
&& readyToUpdateDisplayState()) {
Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenState", state);
- // TODO(b/153319140) remove when we can get this from the above trace invocation
- SystemProperties.set("debug.tracing.screen_state", String.valueOf(state));
+
+ String propertyKey = "debug.tracing.screen_state";
+ String propertyValue = String.valueOf(state);
+ try {
+ // TODO(b/153319140) remove when we can get this from the above trace invocation
+ SystemProperties.set(propertyKey, propertyValue);
+ } catch (RuntimeException e) {
+ Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
+ + " value=" + propertyValue + " " + e.getMessage());
+ }
+
mPowerState.setScreenState(state);
// Tell battery stats about the transition.
noteScreenState(state);
@@ -2029,8 +2032,17 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
}
if (mScreenBrightnessRampAnimator.animateTo(target, sdrTarget, rate)) {
Trace.traceCounter(Trace.TRACE_TAG_POWER, "TargetScreenBrightness", (int) target);
- // TODO(b/153319140) remove when we can get this from the above trace invocation
- SystemProperties.set("debug.tracing.screen_brightness", String.valueOf(target));
+
+ String propertyKey = "debug.tracing.screen_brightness";
+ String propertyValue = String.valueOf(target);
+ try {
+ // TODO(b/153319140) remove when we can get this from the above trace invocation
+ SystemProperties.set(propertyKey, propertyValue);
+ } catch (RuntimeException e) {
+ Slog.e(mTag, "Failed to set a system property: key=" + propertyKey
+ + " value=" + propertyValue + " " + e.getMessage());
+ }
+
noteScreenBrightness(target);
}
}
@@ -2256,17 +2268,17 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
boolean slowChange) {
mBrightnessRangeController.onAmbientLuxChange(ambientLux);
if (nits < 0) {
- mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness);
+ mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
} else {
float brightness = mDisplayBrightnessController.convertToFloatScale(nits);
if (BrightnessUtils.isValidBrightnessValue(brightness)) {
- mDisplayBrightnessController.setBrightnessToFollow(brightness);
+ mDisplayBrightnessController.setBrightnessToFollow(brightness, slowChange);
} else {
// The device does not support nits
- mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness);
+ mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness,
+ slowChange);
}
}
- mBrightnessToFollowSlowChange = slowChange;
sendUpdatePowerState();
}
@@ -2406,7 +2418,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal
pw.println(" mReportedToPolicy="
+ reportedToPolicyToString(mReportedScreenStateToPolicy));
pw.println(" mIsRbcActive=" + mIsRbcActive);
- pw.println(" mBrightnessToFollowSlowChange=" + mBrightnessToFollowSlowChange);
IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
mAutomaticBrightnessStrategy.dump(ipw);
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 11160a532609..c04c2793b3c5 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import android.annotation.Nullable;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.display.BrightnessInfo;
@@ -75,6 +76,8 @@ class HighBrightnessModeController {
private final Injector mInjector;
private HdrListener mHdrListener;
+
+ @Nullable
private HighBrightnessModeData mHbmData;
private HdrBrightnessDeviceConfig mHdrBrightnessCfg;
private IBinder mRegisteredDisplayToken;
@@ -107,7 +110,9 @@ class HighBrightnessModeController {
* If HBM is currently running, this is the start time and set of all events,
* for the current HBM session.
*/
- private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null;
+ @Nullable
+ private HighBrightnessModeMetadata mHighBrightnessModeMetadata;
+
HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken,
String displayUniqueId, float brightnessMin, float brightnessMax,
HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg,
@@ -310,23 +315,29 @@ class HighBrightnessModeController {
pw.println(" mBrightnessMax=" + mBrightnessMax);
pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis()));
pw.println(" mIsTimeAvailable= " + mIsTimeAvailable);
- pw.println(" mRunningStartTimeMillis="
- + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode);
pw.println(" width*height=" + mWidth + "*" + mHeight);
- pw.println(" mEvents=");
- final long currentTime = mClock.uptimeMillis();
- long lastStartTime = currentTime;
- long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
- if (runningStartTimeMillis != -1) {
- lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
- }
- for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
- if (lastStartTime > event.getEndTimeMillis()) {
- pw.println(" event: [normal brightness]: "
- + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
+
+ if (mHighBrightnessModeMetadata != null) {
+ pw.println(" mRunningStartTimeMillis="
+ + TimeUtils.formatUptime(
+ mHighBrightnessModeMetadata.getRunningStartTimeMillis()));
+ pw.println(" mEvents=");
+ final long currentTime = mClock.uptimeMillis();
+ long lastStartTime = currentTime;
+ long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis();
+ if (runningStartTimeMillis != -1) {
+ lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime));
}
- lastStartTime = dumpHbmEvent(pw, event);
+ for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) {
+ if (lastStartTime > event.getEndTimeMillis()) {
+ pw.println(" event: [normal brightness]: "
+ + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis()));
+ }
+ lastStartTime = dumpHbmEvent(pw, event);
+ }
+ } else {
+ pw.println(" mHighBrightnessModeMetadata=null");
}
}
@@ -353,7 +364,7 @@ class HighBrightnessModeController {
}
private boolean deviceSupportsHbm() {
- return mHbmData != null;
+ return mHbmData != null && mHighBrightnessModeMetadata != null;
}
private long calculateRemainingTime(long currentTime) {
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
index 76702d3f6f8c..9e6f0eb93831 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java
@@ -41,6 +41,9 @@ class HighBrightnessModeMetadataMapper {
+ display.getDisplayIdLocked());
return null;
}
+ if (device.getDisplayDeviceConfig().getHighBrightnessModeData() == null) {
+ return null;
+ }
final String uniqueId = device.getUniqueId();
diff --git a/services/core/java/com/android/server/display/TEST_MAPPING b/services/core/java/com/android/server/display/TEST_MAPPING
index c4a566fd7b62..5e4e27069fb1 100644
--- a/services/core/java/com/android/server/display/TEST_MAPPING
+++ b/services/core/java/com/android/server/display/TEST_MAPPING
@@ -1,15 +1,7 @@
{
"presubmit": [
{
- "name": "FrameworksMockingServicesTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"}
- ]
- },
- {
- "name": "FrameworksServicesTests",
+ "name": "DisplayServiceTests",
"options": [
{"include-filter": "com.android.server.display"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
@@ -31,5 +23,14 @@
}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "DisplayServiceTests",
+ "options": [
+ {"include-filter": "com.android.server.display"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
]
-} \ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessUtils.java b/services/core/java/com/android/server/display/brightness/BrightnessUtils.java
index 3fae22434751..8bf675cb33b1 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessUtils.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessUtils.java
@@ -54,6 +54,16 @@ public final class BrightnessUtils {
public static DisplayBrightnessState constructDisplayBrightnessState(
int brightnessChangeReason, float brightness, float sdrBrightness,
String displayBrightnessStrategyName) {
+ return constructDisplayBrightnessState(brightnessChangeReason, brightness, sdrBrightness,
+ displayBrightnessStrategyName, /* slowChange= */ false);
+ }
+
+ /**
+ * A utility to construct the DisplayBrightnessState
+ */
+ public static DisplayBrightnessState constructDisplayBrightnessState(
+ int brightnessChangeReason, float brightness, float sdrBrightness,
+ String displayBrightnessStrategyName, boolean slowChange) {
BrightnessReason brightnessReason = new BrightnessReason();
brightnessReason.setReason(brightnessChangeReason);
return new DisplayBrightnessState.Builder()
@@ -61,6 +71,7 @@ public final class BrightnessUtils {
.setSdrBrightness(sdrBrightness)
.setBrightnessReason(brightnessReason)
.setDisplayBrightnessStrategyName(displayBrightnessStrategyName)
+ .setIsSlowChange(slowChange)
.build();
}
}
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index ffd62a387a64..d6f0098c13cb 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -164,10 +164,10 @@ public final class DisplayBrightnessController {
/**
* Sets the brightness to follow
*/
- public void setBrightnessToFollow(Float brightnessToFollow) {
+ public void setBrightnessToFollow(float brightnessToFollow, boolean slowChange) {
synchronized (mLock) {
mDisplayBrightnessStrategySelector.getFollowerDisplayBrightnessStrategy()
- .setBrightnessToFollow(brightnessToFollow);
+ .setBrightnessToFollow(brightnessToFollow, slowChange);
}
}
diff --git a/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java
index 090ec13570cf..585f576c25c3 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java
@@ -37,9 +37,13 @@ public class FollowerBrightnessStrategy implements DisplayBrightnessStrategy {
// Set to PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no brightness to follow set.
private float mBrightnessToFollow;
+ // Indicates whether we should ramp slowly to the brightness value to follow.
+ private boolean mBrightnessToFollowSlowChange;
+
public FollowerBrightnessStrategy(int displayId) {
mDisplayId = displayId;
mBrightnessToFollow = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+ mBrightnessToFollowSlowChange = false;
}
@Override
@@ -48,7 +52,7 @@ public class FollowerBrightnessStrategy implements DisplayBrightnessStrategy {
// Todo(b/241308599): Introduce a validator class and add validations before setting
// the brightness
return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_FOLLOWER,
- mBrightnessToFollow, mBrightnessToFollow, getName());
+ mBrightnessToFollow, mBrightnessToFollow, getName(), mBrightnessToFollowSlowChange);
}
@Override
@@ -60,8 +64,12 @@ public class FollowerBrightnessStrategy implements DisplayBrightnessStrategy {
return mBrightnessToFollow;
}
- public void setBrightnessToFollow(float brightnessToFollow) {
+ /**
+ * Updates brightness value and brightness slowChange flag
+ **/
+ public void setBrightnessToFollow(float brightnessToFollow, boolean slowChange) {
mBrightnessToFollow = brightnessToFollow;
+ mBrightnessToFollowSlowChange = slowChange;
}
/**
@@ -71,5 +79,6 @@ public class FollowerBrightnessStrategy implements DisplayBrightnessStrategy {
writer.println("FollowerBrightnessStrategy:");
writer.println(" mDisplayId=" + mDisplayId);
writer.println(" mBrightnessToFollow:" + mBrightnessToFollow);
+ writer.println(" mBrightnessToFollowSlowChange:" + mBrightnessToFollowSlowChange);
}
}
diff --git a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
index feebdf1b9799..dfb5f62f15ab 100644
--- a/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
+++ b/services/core/java/com/android/server/display/feature/DeviceConfigParameterProvider.java
@@ -60,6 +60,11 @@ public class DeviceConfigParameterProvider {
DisplayManager.DeviceConfig.KEY_USE_NORMAL_BRIGHTNESS_MODE_CONTROLLER, false);
}
+ public boolean isDisableScreenWakeLocksWhileCachedFeatureEnabled() {
+ return mDeviceConfig.getBoolean(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+ DisplayManager.DeviceConfig.KEY_DISABLE_SCREEN_WAKE_LOCKS_WHILE_CACHED, true);
+ }
+
// feature: smooth_display_feature
// parameter: peak_refresh_rate_default
public float getPeakRefreshRateDefault() {
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index 633bf73120e1..0e8a5fb050dd 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -17,6 +17,8 @@
package com.android.server.dreams;
import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
+import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER;
+import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS;
import android.app.ActivityTaskManager;
import android.app.BroadcastOptions;
@@ -72,6 +74,7 @@ final class DreamController {
private final Handler mHandler;
private final Listener mListener;
private final ActivityTaskManager mActivityTaskManager;
+ private final PowerManager mPowerManager;
private final Intent mDreamingStartedIntent = new Intent(Intent.ACTION_DREAMING_STARTED)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | FLAG_RECEIVER_FOREGROUND);
@@ -84,6 +87,15 @@ final class DreamController {
private final Intent mCloseNotificationShadeIntent;
private final Bundle mCloseNotificationShadeOptions;
+ /**
+ * If this flag is on, we report user activity to {@link PowerManager} so that the screen
+ * doesn't shut off immediately when a dream quits unexpectedly. The device will instead go to
+ * keyguard and time out back to dreaming shortly.
+ *
+ * This allows the dream a second chance to relaunch in case of an app update or other crash.
+ */
+ private final boolean mResetScreenTimeoutOnUnexpectedDreamExit;
+
private DreamRecord mCurrentDream;
// Whether a dreaming started intent has been broadcast.
@@ -101,6 +113,7 @@ final class DreamController {
mHandler = handler;
mListener = listener;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
+ mPowerManager = mContext.getSystemService(PowerManager.class);
mCloseNotificationShadeIntent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
mCloseNotificationShadeIntent.putExtra(EXTRA_REASON_KEY, EXTRA_REASON_VALUE);
mCloseNotificationShadeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -110,6 +123,8 @@ final class DreamController {
EXTRA_REASON_VALUE)
.setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
.toBundle();
+ mResetScreenTimeoutOnUnexpectedDreamExit = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_resetScreenTimeoutOnUnexpectedDreamExit);
}
/**
@@ -235,6 +250,17 @@ final class DreamController {
}
/**
+ * Sends a user activity signal to PowerManager to stop the screen from turning off immediately
+ * if there hasn't been any user interaction in a while.
+ */
+ private void resetScreenTimeout() {
+ Slog.i(TAG, "Resetting screen timeout");
+ long time = SystemClock.uptimeMillis();
+ mPowerManager.userActivity(time, USER_ACTIVITY_EVENT_OTHER,
+ USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS);
+ }
+
+ /**
* Stops dreaming.
*
* The current dream, if any, and any unstopped previous dreams are stopped. The device stops
@@ -448,6 +474,9 @@ final class DreamController {
mHandler.post(() -> {
mService = null;
if (mCurrentDream == DreamRecord.this) {
+ if (mResetScreenTimeoutOnUnexpectedDreamExit) {
+ resetScreenTimeout();
+ }
stopDream(true /*immediate*/, "binder died");
}
});
@@ -473,6 +502,9 @@ final class DreamController {
mHandler.post(() -> {
mService = null;
if (mCurrentDream == DreamRecord.this) {
+ if (mResetScreenTimeoutOnUnexpectedDreamExit) {
+ resetScreenTimeout();
+ }
stopDream(true /*immediate*/, "service disconnected");
}
});
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 3a8f9d5c35e6..5c80291f7f46 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -116,6 +116,7 @@ import com.android.server.DisplayThread;
import com.android.server.LocalServices;
import com.android.server.Watchdog;
import com.android.server.input.InputManagerInternal.LidSwitchCallback;
+import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
import libcore.io.IoUtils;
@@ -165,6 +166,8 @@ public class InputManagerService extends IInputManager.Stub
private final InputManagerHandler mHandler;
private DisplayManagerInternal mDisplayManagerInternal;
+ private InputMethodManagerInternal mInputMethodManagerInternal;
+
// Context cache used for loading pointer resources.
private Context mPointerIconDisplayContext;
@@ -510,6 +513,8 @@ public class InputManagerService extends IInputManager.Stub
}
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+ mInputMethodManagerInternal =
+ LocalServices.getService(InputMethodManagerInternal.class);
mSettingsObserver.registerAndUpdate();
@@ -2790,6 +2795,13 @@ public class InputManagerService extends IInputManager.Stub
yPosition)).sendToTarget();
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ boolean isInputMethodConnectionActive() {
+ return mInputMethodManagerInternal != null
+ && mInputMethodManagerInternal.isAnyInputConnectionActive();
+ }
+
/**
* Callback interface implemented by the Window Manager.
*/
diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
index eb2da340004b..4b30ae530ae6 100644
--- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
+++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java
@@ -21,12 +21,16 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
import android.hardware.input.KeyboardLayout;
import android.icu.util.ULocale;
import android.util.Log;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.InputDevice;
+import android.view.KeyEvent;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.VisibleForTesting;
@@ -36,8 +40,12 @@ import com.android.internal.util.FrameworkStatsLog;
import java.lang.annotation.Retention;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
/**
* Collect Keyboard metrics
@@ -50,13 +58,14 @@ public final class KeyboardMetricsCollector {
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@Retention(SOURCE)
- @IntDef(prefix = { "LAYOUT_SELECTION_CRITERIA_" }, value = {
+ @IntDef(prefix = {"LAYOUT_SELECTION_CRITERIA_"}, value = {
LAYOUT_SELECTION_CRITERIA_USER,
LAYOUT_SELECTION_CRITERIA_DEVICE,
LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD,
LAYOUT_SELECTION_CRITERIA_DEFAULT
})
- public @interface LayoutSelectionCriteria {}
+ public @interface LayoutSelectionCriteria {
+ }
/** Manual selection by user */
public static final int LAYOUT_SELECTION_CRITERIA_USER = 0;
@@ -76,17 +85,301 @@ public final class KeyboardMetricsCollector {
@VisibleForTesting
static final String DEFAULT_LANGUAGE_TAG = "None";
+ public enum KeyboardLogEvent {
+ UNSPECIFIED(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED,
+ "INVALID_KEYBOARD_EVENT"),
+ HOME(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME,
+ "HOME"),
+ RECENT_APPS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS,
+ "RECENT_APPS"),
+ BACK(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BACK,
+ "BACK"),
+ APP_SWITCH(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__APP_SWITCH,
+ "APP_SWITCH"),
+ LAUNCH_ASSISTANT(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_ASSISTANT,
+ "LAUNCH_ASSISTANT"),
+ LAUNCH_VOICE_ASSISTANT(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_VOICE_ASSISTANT,
+ "LAUNCH_VOICE_ASSISTANT"),
+ LAUNCH_SYSTEM_SETTINGS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SYSTEM_SETTINGS,
+ "LAUNCH_SYSTEM_SETTINGS"),
+ TOGGLE_NOTIFICATION_PANEL(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_NOTIFICATION_PANEL,
+ "TOGGLE_NOTIFICATION_PANEL"),
+ TOGGLE_TASKBAR(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR,
+ "TOGGLE_TASKBAR"),
+ TAKE_SCREENSHOT(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT,
+ "TAKE_SCREENSHOT"),
+ OPEN_SHORTCUT_HELPER(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER,
+ "OPEN_SHORTCUT_HELPER"),
+ BRIGHTNESS_UP(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_UP,
+ "BRIGHTNESS_UP"),
+ BRIGHTNESS_DOWN(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__BRIGHTNESS_DOWN,
+ "BRIGHTNESS_DOWN"),
+ KEYBOARD_BACKLIGHT_UP(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_UP,
+ "KEYBOARD_BACKLIGHT_UP"),
+ KEYBOARD_BACKLIGHT_DOWN(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_DOWN,
+ "KEYBOARD_BACKLIGHT_DOWN"),
+ KEYBOARD_BACKLIGHT_TOGGLE(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__KEYBOARD_BACKLIGHT_TOGGLE,
+ "KEYBOARD_BACKLIGHT_TOGGLE"),
+ VOLUME_UP(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_UP,
+ "VOLUME_UP"),
+ VOLUME_DOWN(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_DOWN,
+ "VOLUME_DOWN"),
+ VOLUME_MUTE(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__VOLUME_MUTE,
+ "VOLUME_MUTE"),
+ ALL_APPS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ALL_APPS,
+ "ALL_APPS"),
+ LAUNCH_SEARCH(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_SEARCH,
+ "LAUNCH_SEARCH"),
+ LANGUAGE_SWITCH(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LANGUAGE_SWITCH,
+ "LANGUAGE_SWITCH"),
+ ACCESSIBILITY_ALL_APPS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__ACCESSIBILITY_ALL_APPS,
+ "ACCESSIBILITY_ALL_APPS"),
+ TOGGLE_CAPS_LOCK(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_CAPS_LOCK,
+ "TOGGLE_CAPS_LOCK"),
+ SYSTEM_MUTE(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_MUTE,
+ "SYSTEM_MUTE"),
+ SPLIT_SCREEN_NAVIGATION(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SPLIT_SCREEN_NAVIGATION,
+ "SPLIT_SCREEN_NAVIGATION"),
+ TRIGGER_BUG_REPORT(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TRIGGER_BUG_REPORT,
+ "TRIGGER_BUG_REPORT"),
+ LOCK_SCREEN(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LOCK_SCREEN,
+ "LOCK_SCREEN"),
+ OPEN_NOTES(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_NOTES,
+ "OPEN_NOTES"),
+ TOGGLE_POWER(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_POWER,
+ "TOGGLE_POWER"),
+ SYSTEM_NAVIGATION(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SYSTEM_NAVIGATION,
+ "SYSTEM_NAVIGATION"),
+ SLEEP(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SLEEP,
+ "SLEEP"),
+ WAKEUP(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__WAKEUP,
+ "WAKEUP"),
+ MEDIA_KEY(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MEDIA_KEY,
+ "MEDIA_KEY"),
+ LAUNCH_DEFAULT_BROWSER(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_BROWSER,
+ "LAUNCH_DEFAULT_BROWSER"),
+ LAUNCH_DEFAULT_EMAIL(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_EMAIL,
+ "LAUNCH_DEFAULT_EMAIL"),
+ LAUNCH_DEFAULT_CONTACTS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CONTACTS,
+ "LAUNCH_DEFAULT_CONTACTS"),
+ LAUNCH_DEFAULT_CALENDAR(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALENDAR,
+ "LAUNCH_DEFAULT_CALENDAR"),
+ LAUNCH_DEFAULT_CALCULATOR(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_CALCULATOR,
+ "LAUNCH_DEFAULT_CALCULATOR"),
+ LAUNCH_DEFAULT_MUSIC(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MUSIC,
+ "LAUNCH_DEFAULT_MUSIC"),
+ LAUNCH_DEFAULT_MAPS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MAPS,
+ "LAUNCH_DEFAULT_MAPS"),
+ LAUNCH_DEFAULT_MESSAGING(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_MESSAGING,
+ "LAUNCH_DEFAULT_MESSAGING"),
+ LAUNCH_DEFAULT_GALLERY(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_GALLERY,
+ "LAUNCH_DEFAULT_GALLERY"),
+ LAUNCH_DEFAULT_FILES(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FILES,
+ "LAUNCH_DEFAULT_FILES"),
+ LAUNCH_DEFAULT_WEATHER(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_WEATHER,
+ "LAUNCH_DEFAULT_WEATHER"),
+ LAUNCH_DEFAULT_FITNESS(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_DEFAULT_FITNESS,
+ "LAUNCH_DEFAULT_FITNESS"),
+ LAUNCH_APPLICATION_BY_PACKAGE_NAME(
+ FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__LAUNCH_APPLICATION_BY_PACKAGE_NAME,
+ "LAUNCH_APPLICATION_BY_PACKAGE_NAME");
+
+ private final int mValue;
+ private final String mName;
+
+ private static final SparseArray<KeyboardLogEvent> VALUE_TO_ENUM_MAP = new SparseArray<>();
+
+ static {
+ for (KeyboardLogEvent type : KeyboardLogEvent.values()) {
+ VALUE_TO_ENUM_MAP.put(type.mValue, type);
+ }
+ }
+
+ KeyboardLogEvent(int enumValue, String enumName) {
+ mValue = enumValue;
+ mName = enumName;
+ }
+
+ public int getIntValue() {
+ return mValue;
+ }
+
+ /**
+ * Convert int value to corresponding KeyboardLogEvent enum. If can't find any matching
+ * value will return {@code null}
+ */
+ @Nullable
+ public static KeyboardLogEvent from(int value) {
+ return VALUE_TO_ENUM_MAP.get(value);
+ }
+
+ /**
+ * Find KeyboardLogEvent corresponding to volume up/down/mute key events.
+ */
+ @Nullable
+ public static KeyboardLogEvent getVolumeEvent(int keycode) {
+ switch (keycode) {
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ return VOLUME_DOWN;
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ return VOLUME_UP;
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ return VOLUME_MUTE;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Find KeyboardLogEvent corresponding to brightness up/down key events.
+ */
+ @Nullable
+ public static KeyboardLogEvent getBrightnessEvent(int keycode) {
+ switch (keycode) {
+ case KeyEvent.KEYCODE_BRIGHTNESS_DOWN:
+ return BRIGHTNESS_DOWN;
+ case KeyEvent.KEYCODE_BRIGHTNESS_UP:
+ return BRIGHTNESS_UP;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Find KeyboardLogEvent corresponding to intent filter category. Returns
+ * {@code null if no matching event found}
+ */
+ @Nullable
+ public static KeyboardLogEvent getLogEventFromIntent(Intent intent) {
+ Intent selectorIntent = intent.getSelector();
+ if (selectorIntent != null) {
+ Set<String> selectorCategories = selectorIntent.getCategories();
+ if (selectorCategories != null && !selectorCategories.isEmpty()) {
+ for (String intentCategory : selectorCategories) {
+ KeyboardLogEvent logEvent = getEventFromSelectorCategory(intentCategory);
+ if (logEvent == null) {
+ continue;
+ }
+ return logEvent;
+ }
+ }
+ }
+
+ Set<String> intentCategories = intent.getCategories();
+ if (intentCategories == null || intentCategories.isEmpty()
+ || !intentCategories.contains(Intent.CATEGORY_LAUNCHER)) {
+ return null;
+ }
+ if (intent.getComponent() == null) {
+ return null;
+ }
+
+ // TODO(b/280423320): Add new field package name associated in the
+ // KeyboardShortcutEvent atom and log it accordingly.
+ return LAUNCH_APPLICATION_BY_PACKAGE_NAME;
+ }
+
+ @Nullable
+ private static KeyboardLogEvent getEventFromSelectorCategory(String category) {
+ switch (category) {
+ case Intent.CATEGORY_APP_BROWSER:
+ return LAUNCH_DEFAULT_BROWSER;
+ case Intent.CATEGORY_APP_EMAIL:
+ return LAUNCH_DEFAULT_EMAIL;
+ case Intent.CATEGORY_APP_CONTACTS:
+ return LAUNCH_DEFAULT_CONTACTS;
+ case Intent.CATEGORY_APP_CALENDAR:
+ return LAUNCH_DEFAULT_CALENDAR;
+ case Intent.CATEGORY_APP_CALCULATOR:
+ return LAUNCH_DEFAULT_CALCULATOR;
+ case Intent.CATEGORY_APP_MUSIC:
+ return LAUNCH_DEFAULT_MUSIC;
+ case Intent.CATEGORY_APP_MAPS:
+ return LAUNCH_DEFAULT_MAPS;
+ case Intent.CATEGORY_APP_MESSAGING:
+ return LAUNCH_DEFAULT_MESSAGING;
+ case Intent.CATEGORY_APP_GALLERY:
+ return LAUNCH_DEFAULT_GALLERY;
+ case Intent.CATEGORY_APP_FILES:
+ return LAUNCH_DEFAULT_FILES;
+ case Intent.CATEGORY_APP_WEATHER:
+ return LAUNCH_DEFAULT_WEATHER;
+ case Intent.CATEGORY_APP_FITNESS:
+ return LAUNCH_DEFAULT_FITNESS;
+ default:
+ return null;
+ }
+ }
+ }
+
/**
* Log keyboard system shortcuts for the proto
* {@link com.android.os.input.KeyboardSystemsEventReported}
* defined in "stats/atoms/input/input_extension_atoms.proto"
*/
- public static void logKeyboardSystemsEventReportedAtom(InputDevice inputDevice,
- int keyboardSystemEvent, int[] keyCode, int modifierState) {
+ public static void logKeyboardSystemsEventReportedAtom(@Nullable InputDevice inputDevice,
+ @Nullable KeyboardLogEvent keyboardSystemEvent, int modifierState, int... keyCodes) {
+ // Logging Keyboard system event only for an external HW keyboard. We should not log events
+ // for virtual keyboards or internal Key events.
+ if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
+ return;
+ }
int vendorId = inputDevice.getVendorId();
int productId = inputDevice.getProductId();
+ if (keyboardSystemEvent == null) {
+ Slog.w(TAG, "Invalid keyboard event logging, keycode = " + Arrays.toString(keyCodes)
+ + ", modifier state = " + modifierState);
+ return;
+ }
FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED,
- vendorId, productId, keyboardSystemEvent, keyCode, modifierState);
+ vendorId, productId, keyboardSystemEvent.getIntValue(), keyCodes, modifierState);
+
+ if (DEBUG) {
+ Slog.d(TAG, "Logging Keyboard system event: " + keyboardSystemEvent.mName);
+ }
}
/**
@@ -94,8 +387,8 @@ public final class KeyboardMetricsCollector {
* {@link com.android.os.input.KeyboardConfigured} atom
*
* @param event {@link KeyboardConfigurationEvent} contains information about keyboard
- * configuration. Use {@link KeyboardConfigurationEvent.Builder} to create the
- * configuration event to log.
+ * configuration. Use {@link KeyboardConfigurationEvent.Builder} to create the
+ * configuration event to log.
*/
public static void logKeyboardConfiguredAtom(KeyboardConfigurationEvent event) {
// Creating proto to log nested field KeyboardLayoutConfig in atom
@@ -241,7 +534,7 @@ public final class KeyboardMetricsCollector {
KeyboardLayout selectedLayout = mSelectedLayoutList.get(i);
@LayoutSelectionCriteria int layoutSelectionCriteria =
mLayoutSelectionCriteriaList.get(i);
- InputMethodSubtype imeSubtype = mImeSubtypeList.get(i);
+ InputMethodSubtype imeSubtype = mImeSubtypeList.get(i);
String keyboardLanguageTag = mInputDevice.getKeyboardLanguageTag();
keyboardLanguageTag = keyboardLanguageTag == null ? DEFAULT_LANGUAGE_TAG
: keyboardLanguageTag;
@@ -328,4 +621,3 @@ public final class KeyboardMetricsCollector {
|| layoutSelectionCriteria == LAYOUT_SELECTION_CRITERIA_DEFAULT;
}
}
-
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index a1b67e105dd4..f1698dd0f025 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -37,6 +37,7 @@ import android.os.ResultReceiver;
import android.util.EventLog;
import android.util.Slog;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodManager;
import com.android.internal.annotations.GuardedBy;
@@ -75,7 +76,8 @@ final class DefaultImeVisibilityApplier implements ImeVisibilityApplier {
@GuardedBy("ImfLock.class")
@Override
public void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
- int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
if (curMethod != null) {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index c53f1a52306d..b12a816738da 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -30,6 +30,7 @@ import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ImeTracker;
import android.view.inputmethod.InputBinding;
+import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodSubtype;
import android.window.ImeOnBackInvokedDispatcher;
@@ -198,8 +199,8 @@ final class IInputMethodInvoker {
// TODO(b/192412909): Convert this back to void method
@AnyThread
- boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
- ResultReceiver resultReceiver) {
+ boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+ @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
try {
mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
} catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
index 27f6a89a73b3..29fa36982351 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
import com.android.internal.inputmethod.SoftInputShowHideReason;
@@ -34,13 +35,13 @@ interface ImeVisibilityApplier {
*
* @param showInputToken A token that represents the requester to show IME.
* @param statsToken A token that tracks the progress of an IME request.
- * @param showFlags Provides additional operating flags to show IME.
* @param resultReceiver If non-null, this will be called back to the caller when
* it has processed request to tell what it has done.
* @param reason The reason for requesting to show IME.
*/
default void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
- int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
+ @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {}
/**
* Performs hiding IME to the given window
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index f012d917b05e..9ad4628596fc 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -221,17 +221,21 @@ public final class ImeVisibilityStateComputer {
/**
* Called when {@link InputMethodManagerService} is processing the show IME request.
- * @param statsToken The token for tracking this show request
- * @param showFlags The additional operation flags to indicate whether this show request mode is
- * implicit or explicit.
- * @return {@code true} when the computer has proceed this show request operation.
+ *
+ * @param statsToken The token for tracking this show request.
+ * @return {@code true} when the show request can proceed.
*/
- boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken, int showFlags) {
+ boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken,
+ @InputMethodManager.ShowFlags int showFlags) {
if (mPolicy.mA11yRequestingNoSoftKeyboard || mPolicy.mImeHiddenByDisplayPolicy) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
return false;
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
+ // We only "set" the state corresponding to the flags, as this will be reset
+ // in clearImeShowFlags during a hide request.
+ // Thus, we keep the strongest values set (e.g. an implicit show right after
+ // an explicit show will still be considered explicit, likewise for forced).
if ((showFlags & InputMethodManager.SHOW_FORCED) != 0) {
mRequestedShowExplicitly = true;
mShowForced = true;
@@ -243,12 +247,12 @@ public final class ImeVisibilityStateComputer {
/**
* Called when {@link InputMethodManagerService} is processing the hide IME request.
- * @param statsToken The token for tracking this hide request
- * @param hideFlags The additional operation flags to indicate whether this hide request mode is
- * implicit or explicit.
- * @return {@code true} when the computer has proceed this hide request operations.
+ *
+ * @param statsToken The token for tracking this hide request.
+ * @return {@code true} when the hide request can proceed.
*/
- boolean canHideIme(@NonNull ImeTracker.Token statsToken, int hideFlags) {
+ boolean canHideIme(@NonNull ImeTracker.Token statsToken,
+ @InputMethodManager.HideFlags int hideFlags) {
if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
&& (mRequestedShowExplicitly || mShowForced)) {
if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
@@ -264,13 +268,31 @@ public final class ImeVisibilityStateComputer {
return true;
}
- int getImeShowFlags() {
+ /**
+ * Returns the show flags for IME. This translates from {@link InputMethodManager.ShowFlags}
+ * to {@link InputMethod.ShowFlags}.
+ */
+ @InputMethod.ShowFlags
+ int getShowFlagsForInputMethodServiceOnly() {
int flags = 0;
if (mShowForced) {
flags |= InputMethod.SHOW_FORCED | InputMethod.SHOW_EXPLICIT;
} else if (mRequestedShowExplicitly) {
flags |= InputMethod.SHOW_EXPLICIT;
- } else {
+ }
+ return flags;
+ }
+
+ /**
+ * Returns the show flags for IMM. This translates from {@link InputMethod.ShowFlags}
+ * to {@link InputMethodManager.ShowFlags}.
+ */
+ @InputMethodManager.ShowFlags
+ int getShowFlags() {
+ int flags = 0;
+ if (mShowForced) {
+ flags |= InputMethodManager.SHOW_FORCED;
+ } else if (!mRequestedShowExplicitly) {
flags |= InputMethodManager.SHOW_IMPLICIT;
}
return flags;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index 8c7658e53dcd..08503cb2e9f8 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -186,6 +186,12 @@ public abstract class InputMethodManagerInternal {
public abstract void switchKeyboardLayout(int direction);
/**
+ * Returns true if any InputConnection is currently active.
+ * {@hide}
+ */
+ public abstract boolean isAnyInputConnectionActive();
+
+ /**
* Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing.
*/
private static final InputMethodManagerInternal NOP =
@@ -268,6 +274,11 @@ public abstract class InputMethodManagerInternal {
@Override
public void switchKeyboardLayout(int direction) {
}
+
+ @Override
+ public boolean isAnyInputConnectionActive() {
+ return false;
+ }
};
/**
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 7bda2c1fa5ab..cfcb4620bf25 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2468,7 +2468,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
final ImeTracker.Token statsToken = mCurStatsToken;
mCurStatsToken = null;
showCurrentInputLocked(mCurFocusedWindow, statsToken,
- mVisibilityStateComputer.getImeShowFlags(),
+ mVisibilityStateComputer.getShowFlags(),
null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
}
@@ -3404,8 +3404,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
@Override
public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+ int lastClickTooType, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
@@ -3578,15 +3579,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
@GuardedBy("ImfLock.class")
boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
- int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @InputMethodManager.ShowFlags int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
return showCurrentInputLocked(windowToken, statsToken, flags,
MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
}
@GuardedBy("ImfLock.class")
private boolean showCurrentInputLocked(IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+ int lastClickToolType, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
statsToken = createStatsTokenForFocusedClient(true /* show */,
@@ -3617,7 +3620,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
curMethod.updateEditorToolType(lastClickToolType);
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
- mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason);
+ mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
+ resultReceiver, reason);
mVisibilityStateComputer.setInputShown(true);
return true;
} else {
@@ -3629,8 +3633,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
@Override
public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
- @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
+ @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
int uid = Binder.getCallingUid();
ImeTracing.getInstance().triggerManagerServiceDump(
"InputMethodManagerService#hideSoftInput");
@@ -3660,7 +3664,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
@GuardedBy("ImfLock.class")
boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
- int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver,
+ @SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
statsToken = createStatsTokenForFocusedClient(false /* show */,
@@ -4847,7 +4852,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
}
@BinderThread
- private void hideMySoftInput(@NonNull IBinder token, int flags,
+ private void hideMySoftInput(@NonNull IBinder token, @InputMethodManager.HideFlags int flags,
@SoftInputShowHideReason int reason) {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
@@ -4869,7 +4874,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
}
@BinderThread
- private void showMySoftInput(@NonNull IBinder token, int flags) {
+ private void showMySoftInput(@NonNull IBinder token, @InputMethodManager.ShowFlags int flags) {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput");
synchronized (ImfLock.class) {
@@ -5932,6 +5937,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
}
}
}
+
+ /**
+ * Returns true if any InputConnection is currently active.
+ */
+ @Override
+ public boolean isAnyInputConnectionActive() {
+ return mCurInputConnection != null;
+ }
}
@BinderThread
@@ -6828,8 +6841,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
@BinderThread
@Override
- public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason,
- AndroidFuture future /* T=Void */) {
+ public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
+ @SoftInputShowHideReason int reason, AndroidFuture future /* T=Void */) {
@SuppressWarnings("unchecked")
final AndroidFuture<Void> typedFuture = future;
try {
@@ -6842,7 +6855,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub
@BinderThread
@Override
- public void showMySoftInput(int flags, AndroidFuture future /* T=Void */) {
+ public void showMySoftInput(@InputMethodManager.ShowFlags int flags,
+ AndroidFuture future /* T=Void */) {
@SuppressWarnings("unchecked")
final AndroidFuture<Void> typedFuture = future;
try {
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index a96e4adf1fee..0616f4e9d5ac 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -844,6 +844,10 @@ public class LockSettingsService extends ILockSettings.Stub {
getAuthSecretHal();
mDeviceProvisionedObserver.onSystemReady();
+ // Work around an issue in PropertyInvalidatedCache where the cache doesn't work until the
+ // first invalidation. This can be removed if PropertyInvalidatedCache is fixed.
+ LockPatternUtils.invalidateCredentialTypeCache();
+
// TODO: maybe skip this for split system user mode.
mStorage.prefetchUser(UserHandle.USER_SYSTEM);
mBiometricDeferredQueue.systemReady(mInjector.getFingerprintManager(),
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 398e470d9fda..61d0afeb7717 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -117,6 +117,10 @@ public final class MediaProjectionManagerService extends SystemService
// WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService
// See mediaprojection.md
private final Object mLock = new Object();
+ // A handler for posting tasks that must interact with a service holding another lock,
+ // especially for services that will eventually acquire the WindowManager lock.
+ @NonNull private final Handler mHandler;
+
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
@@ -145,6 +149,8 @@ public final class MediaProjectionManagerService extends SystemService
super(context);
mContext = context;
mInjector = injector;
+ // Post messages on the main thread; no need for a separate thread.
+ mHandler = new Handler(Looper.getMainLooper());
mClock = injector.createClock();
mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
mCallbackDelegate = new CallbackDelegate(injector.createCallbackLooper());
@@ -243,14 +249,17 @@ public final class MediaProjectionManagerService extends SystemService
if (!mProjectionGrant.requiresForegroundService()) {
return;
}
+ }
- if (mActivityManagerInternal.hasRunningForegroundService(
- uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
- // If there is any process within this UID running a FGS
- // with the mediaProjection type, that's Okay.
- return;
- }
+ // Run outside the lock when calling into ActivityManagerService.
+ if (mActivityManagerInternal.hasRunningForegroundService(
+ uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
+ // If there is any process within this UID running a FGS
+ // with the mediaProjection type, that's Okay.
+ return;
+ }
+ synchronized (mLock) {
mProjectionGrant.stop();
}
}
@@ -867,7 +876,6 @@ public final class MediaProjectionManagerService extends SystemService
mTargetSdkVersion = targetSdkVersion;
mIsPrivileged = isPrivileged;
mCreateTimeMs = mClock.uptimeMillis();
- // TODO(b/267740338): Add unit test.
mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
MEDIA_PROJECTION_TOKEN_EVENT_CREATED);
}
@@ -924,6 +932,10 @@ public final class MediaProjectionManagerService extends SystemService
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
+ // Cache result of calling into ActivityManagerService outside of the lock, to prevent
+ // deadlock with WindowManagerService.
+ final boolean hasFGS = mActivityManagerInternal.hasRunningForegroundService(
+ uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
synchronized (mLock) {
if (isCurrentProjection(asBinder())) {
Slog.w(TAG, "UID " + Binder.getCallingUid()
@@ -935,9 +947,7 @@ public final class MediaProjectionManagerService extends SystemService
}
if (REQUIRE_FG_SERVICE_FOR_PROJECTION
- && requiresForegroundService()
- && !mActivityManagerInternal.hasRunningForegroundService(
- uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) {
+ && requiresForegroundService() && !hasFGS) {
throw new SecurityException("Media projections require a foreground service"
+ " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION");
}
@@ -1026,10 +1036,11 @@ public final class MediaProjectionManagerService extends SystemService
mToken = null;
unregisterCallback(mCallback);
mCallback = null;
- // TODO(b/267740338): Add unit test.
- mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
- MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED);
}
+ // Run on a separate thread, to ensure no lock is held when calling into
+ // ActivityManagerService.
+ mHandler.post(() -> mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
+ MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED));
}
@Override // Binder call
diff --git a/services/core/java/com/android/server/media/projection/mediaprojection.md b/services/core/java/com/android/server/media/projection/mediaprojection.md
index bccdf3411903..34e7ecc6c6c5 100644
--- a/services/core/java/com/android/server/media/projection/mediaprojection.md
+++ b/services/core/java/com/android/server/media/projection/mediaprojection.md
@@ -11,6 +11,11 @@ Calls must follow the below invocation order while holding locks:
`WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService`
+`MediaProjectionManagerService` should never lock when calling into a service that may acquire
+the `WindowManagerService` global lock (for example,
+`MediaProjectionManagerService -> ActivityManagerService` may result in deadlock, since
+`ActivityManagerService -> WindowManagerService`).
+
### Justification
`MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6df38098205a..c2b21beb10df 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -118,7 +118,6 @@ import static android.service.notification.NotificationListenerService.TRIM_FULL
import static android.service.notification.NotificationListenerService.TRIM_LIGHT;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
-import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ALLOW_DISMISS_ONGOING;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.WAKE_LOCK_FOR_POSTING_NOTIFICATION;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES;
@@ -1021,6 +1020,7 @@ public class NotificationManagerService extends SystemService {
}
mAssistants.resetDefaultAssistantsIfNecessary();
+ mPreferencesHelper.syncChannelsBypassingDnd();
}
@VisibleForTesting
@@ -1221,8 +1221,7 @@ public class NotificationManagerService extends SystemService {
}
}
- int mustNotHaveFlags = mFlagResolver.isEnabled(ALLOW_DISMISS_ONGOING)
- ? FLAG_NO_DISMISS : FLAG_ONGOING_EVENT;
+ int mustNotHaveFlags = FLAG_NO_DISMISS;
cancelNotification(callingUid, callingPid, pkg, tag, id,
/* mustHaveFlags= */ 0,
/* mustNotHaveFlags= */ mustNotHaveFlags,
@@ -1861,6 +1860,7 @@ public class NotificationManagerService extends SystemService {
mConditionProviders.onUserSwitched(userId);
mListeners.onUserSwitched(userId);
mZenModeHelper.onUserSwitched(userId);
+ mPreferencesHelper.syncChannelsBypassingDnd();
}
// assistant is the only thing that cares about managed profiles specifically
mAssistants.onUserSwitched(userId);
@@ -3291,6 +3291,11 @@ public class NotificationManagerService extends SystemService {
return new MultiRateLimiter.Builder(getContext()).addRateLimits(TOAST_RATE_LIMITS).build();
}
+ protected int checkComponentPermission(String permission, int uid, int owningUid,
+ boolean exported) {
+ return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported);
+ }
+
@VisibleForTesting
final IBinder mService = new INotificationManager.Stub() {
// Toasts
@@ -3560,6 +3565,7 @@ public class NotificationManagerService extends SystemService {
}
mPermissionHelper.setNotificationPermission(
pkg, UserHandle.getUserId(uid), enabled, true);
+ sendAppBlockStateChangedBroadcast(pkg, uid, !enabled);
mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_BAN_APP_NOTES)
.setType(MetricsEvent.TYPE_ACTION)
@@ -3567,8 +3573,7 @@ public class NotificationManagerService extends SystemService {
.setSubtype(enabled ? 1 : 0));
mNotificationChannelLogger.logAppNotificationsAllowed(uid, pkg, enabled);
- // Outstanding notifications from this package will be cancelled, and the package will
- // be sent the ACTION_APP_BLOCK_STATE_CHANGED broadcast, as soon as we get the
+ // Outstanding notifications from this package will be cancelled as soon as we get the
// callback from AppOpsManager.
}
@@ -5244,10 +5249,11 @@ public class NotificationManagerService extends SystemService {
}
private boolean checkPolicyAccess(String pkg) {
+ final int uid;
try {
- int uid = getContext().getPackageManager().getPackageUidAsUser(pkg,
+ uid = getContext().getPackageManager().getPackageUidAsUser(pkg,
UserHandle.getCallingUserId());
- if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
+ if (PackageManager.PERMISSION_GRANTED == checkComponentPermission(
android.Manifest.permission.MANAGE_NOTIFICATIONS, uid,
-1, true)) {
return true;
@@ -5258,8 +5264,8 @@ public class NotificationManagerService extends SystemService {
//TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
return checkPackagePolicyAccess(pkg)
|| mListeners.isComponentEnabledForPackage(pkg)
- || (mDpm != null && (mDpm.isActiveProfileOwner(Binder.getCallingUid())
- || mDpm.isActiveDeviceOwner(Binder.getCallingUid())));
+ || (mDpm != null && (mDpm.isActiveProfileOwner(uid)
+ || mDpm.isActiveDeviceOwner(uid)));
}
@Override
@@ -5893,13 +5899,15 @@ public class NotificationManagerService extends SystemService {
};
private void handleNotificationPermissionChange(String pkg, @UserIdInt int userId) {
+ if (!mUmInternal.isUserInitialized(userId)) {
+ return; // App-op "updates" are sent when starting a new user the first time.
+ }
int uid = mPackageManagerInternal.getPackageUid(pkg, 0, userId);
if (uid == INVALID_UID) {
Log.e(TAG, String.format("No uid found for %s, %s!", pkg, userId));
return;
}
boolean hasPermission = mPermissionHelper.hasPermission(uid);
- sendAppBlockStateChangedBroadcast(pkg, uid, !hasPermission);
if (!hasPermission) {
cancelAllNotificationsInt(MY_UID, MY_PID, pkg, /* channelId= */ null,
/* mustHaveFlags= */ 0, /* mustNotHaveFlags= */ 0, userId,
@@ -6886,13 +6894,11 @@ public class NotificationManagerService extends SystemService {
}
// Only notifications that can be non-dismissible can have the flag FLAG_NO_DISMISS
- if (mFlagResolver.isEnabled(ALLOW_DISMISS_ONGOING)) {
- if (((notification.flags & FLAG_ONGOING_EVENT) > 0)
- && canBeNonDismissible(ai, notification)) {
- notification.flags |= FLAG_NO_DISMISS;
- } else {
- notification.flags &= ~FLAG_NO_DISMISS;
- }
+ if (((notification.flags & FLAG_ONGOING_EVENT) > 0)
+ && canBeNonDismissible(ai, notification)) {
+ notification.flags |= FLAG_NO_DISMISS;
+ } else {
+ notification.flags &= ~FLAG_NO_DISMISS;
}
int canColorize = getContext().checkPermission(
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 1818d25ae59e..1bb10929135c 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -202,7 +202,7 @@ public class PreferencesHelper implements RankingConfig {
private SparseBooleanArray mLockScreenShowNotifications;
private SparseBooleanArray mLockScreenPrivateNotifications;
private boolean mIsMediaNotificationFilteringEnabled = DEFAULT_MEDIA_NOTIFICATION_FILTERING;
- private boolean mAreChannelsBypassingDnd;
+ private boolean mCurrentUserHasChannelsBypassingDnd;
private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
private boolean mShowReviewPermissionsNotification;
@@ -230,7 +230,6 @@ public class PreferencesHelper implements RankingConfig {
updateBadgingEnabled();
updateBubblesEnabled();
updateMediaNotificationFilteringEnabled();
- syncChannelsBypassingDnd(Process.SYSTEM_UID, true); // init comes from system
}
public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
@@ -893,7 +892,7 @@ public class PreferencesHelper implements RankingConfig {
r.groups.put(group.getId(), group);
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
}
@@ -972,7 +971,7 @@ public class PreferencesHelper implements RankingConfig {
existing.setBypassDnd(bypassDnd);
needsPolicyFileChange = true;
- if (bypassDnd != mAreChannelsBypassingDnd
+ if (bypassDnd != mCurrentUserHasChannelsBypassingDnd
|| previousExistingImportance != existing.getImportance()) {
needsDndChange = true;
}
@@ -1031,7 +1030,7 @@ public class PreferencesHelper implements RankingConfig {
}
r.channels.put(channel.getId(), channel);
- if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
+ if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) {
needsDndChange = true;
}
MetricsLogger.action(getChannelLog(channel, pkg).setType(
@@ -1041,7 +1040,7 @@ public class PreferencesHelper implements RankingConfig {
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return needsPolicyFileChange;
@@ -1127,14 +1126,14 @@ public class PreferencesHelper implements RankingConfig {
// relevantly affected without the parent channel already having been.
}
- if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd
+ if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd
|| channel.getImportance() != updatedChannel.getImportance()) {
needsDndChange = true;
changed = true;
}
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
if (changed) {
updateConfig();
@@ -1321,7 +1320,7 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (channelBypassedDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannel;
}
@@ -1538,7 +1537,7 @@ public class PreferencesHelper implements RankingConfig {
}
}
if (groupBypassedDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannels;
}
@@ -1685,8 +1684,8 @@ public class PreferencesHelper implements RankingConfig {
}
}
}
- if (!deletedChannelIds.isEmpty() && mAreChannelsBypassingDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannelIds;
}
@@ -1788,21 +1787,28 @@ public class PreferencesHelper implements RankingConfig {
}
/**
- * Syncs {@link #mAreChannelsBypassingDnd} with the current user's notification policy before
- * updating
+ * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification
+ * policy before updating. Must be called:
+ * <ul>
+ * <li>On system init, after channels and DND configurations are loaded.</li>
+ * <li>When the current user changes, after the corresponding DND config is loaded.</li>
+ * </ul>
*/
- private void syncChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
- mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
- & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1;
+ void syncChannelsBypassingDnd() {
+ mCurrentUserHasChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
+ & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID,
+ /* fromSystemOrSystemUi= */ true);
}
/**
- * Updates the user's NotificationPolicy based on whether the current userId
- * has channels bypassing DND
+ * Updates the user's NotificationPolicy based on whether the current userId has channels
+ * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
+ * when the current user is switched.
*/
- private void updateChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
+ private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
+ boolean fromSystemOrSystemUi) {
ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
final int currentUserId = getCurrentUser();
@@ -1817,7 +1823,7 @@ public class PreferencesHelper implements RankingConfig {
for (NotificationChannel channel : r.channels.values()) {
if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) {
- candidatePkgs.add(new Pair(r.pkg, r.uid));
+ candidatePkgs.add(new Pair<>(r.pkg, r.uid));
break;
}
}
@@ -1830,9 +1836,9 @@ public class PreferencesHelper implements RankingConfig {
}
}
boolean haveBypassingApps = candidatePkgs.size() > 0;
- if (mAreChannelsBypassingDnd != haveBypassingApps) {
- mAreChannelsBypassingDnd = haveBypassingApps;
- updateZenPolicy(mAreChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
+ if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
+ mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
+ updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
}
}
@@ -1869,7 +1875,7 @@ public class PreferencesHelper implements RankingConfig {
}
public boolean areChannelsBypassingDnd() {
- return mAreChannelsBypassingDnd;
+ return mCurrentUserHasChannelsBypassingDnd;
}
/**
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 36a0b0c0d8e9..1f5bd3e0cc60 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -75,6 +75,7 @@ import android.util.StatsEvent;
import android.util.proto.ProtoOutputStream;
import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
import com.android.internal.logging.MetricsLogger;
@@ -108,30 +109,34 @@ public class ZenModeHelper {
static final int RULE_LIMIT_PER_PACKAGE = 100;
// pkg|userId => uid
- protected final ArrayMap<String, Integer> mRulesUidCache = new ArrayMap<>();
+ @VisibleForTesting protected final ArrayMap<String, Integer> mRulesUidCache = new ArrayMap<>();
private final Context mContext;
private final H mHandler;
private final SettingsObserver mSettingsObserver;
private final AppOpsManager mAppOps;
- @VisibleForTesting protected final NotificationManager mNotificationManager;
+ private final NotificationManager mNotificationManager;
private final SysUiStatsEvent.BuilderFactory mStatsEventBuilderFactory;
- @VisibleForTesting protected ZenModeConfig mDefaultConfig;
+ private ZenModeConfig mDefaultConfig;
private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
private final ZenModeFiltering mFiltering;
- protected final RingerModeDelegate mRingerModeDelegate = new
+ private final RingerModeDelegate mRingerModeDelegate = new
RingerModeDelegate();
@VisibleForTesting protected final ZenModeConditions mConditions;
- Object mConfigsLock = new Object();
+ private final Object mConfigsArrayLock = new Object();
+ @GuardedBy("mConfigsArrayLock")
@VisibleForTesting final SparseArray<ZenModeConfig> mConfigs = new SparseArray<>();
private final Metrics mMetrics = new Metrics();
private final ConditionProviders.Config mServiceConfig;
- private SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
- @VisibleForTesting protected ZenModeEventLogger mZenModeEventLogger;
+ private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
+ private final ZenModeEventLogger mZenModeEventLogger;
@VisibleForTesting protected int mZenMode;
@VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy;
private int mUser = UserHandle.USER_SYSTEM;
+
+ private final Object mConfigLock = new Object();
+ @GuardedBy("mConfigLock")
@VisibleForTesting protected ZenModeConfig mConfig;
@VisibleForTesting protected AudioManagerInternal mAudioManager;
protected PackageManager mPm;
@@ -159,7 +164,7 @@ public class ZenModeHelper {
mDefaultConfig = readDefaultConfig(mContext.getResources());
updateDefaultAutomaticRuleNames();
mConfig = mDefaultConfig.copy();
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
mConfigs.put(UserHandle.USER_SYSTEM, mConfig);
}
mConsolidatedPolicy = mConfig.toNotificationPolicy();
@@ -186,7 +191,7 @@ public class ZenModeHelper {
public boolean matchesCallFilter(UserHandle userHandle, Bundle extras,
ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity,
int callingUid) {
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
return ZenModeFiltering.matchesCallFilter(mContext, mZenMode, mConsolidatedPolicy,
userHandle, extras, validator, contactsTimeoutMs, timeoutAffinity,
callingUid);
@@ -206,7 +211,7 @@ public class ZenModeHelper {
}
public boolean shouldIntercept(NotificationRecord record) {
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
return mFiltering.shouldIntercept(mZenMode, mConsolidatedPolicy, record);
}
}
@@ -221,7 +226,7 @@ public class ZenModeHelper {
public void initZenMode() {
if (DEBUG) Log.d(TAG, "initZenMode");
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
// "update" config to itself, which will have no effect in the case where a config
// was read in via XML, but will initialize zen mode if nothing was read in and the
// config remains the default.
@@ -250,7 +255,7 @@ public class ZenModeHelper {
public void onUserRemoved(int user) {
if (user < UserHandle.USER_SYSTEM) return;
if (DEBUG) Log.d(TAG, "onUserRemoved u=" + user);
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
mConfigs.remove(user);
}
}
@@ -268,7 +273,7 @@ public class ZenModeHelper {
mUser = user;
if (DEBUG) Log.d(TAG, reason + " u=" + user);
ZenModeConfig config = null;
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
if (mConfigs.get(user) != null) {
config = mConfigs.get(user).copy();
}
@@ -278,7 +283,7 @@ public class ZenModeHelper {
config = mDefaultConfig.copy();
config.user = user;
}
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
setConfigLocked(config, null, reason, Process.SYSTEM_UID, true);
}
cleanUpZenRules();
@@ -314,7 +319,7 @@ public class ZenModeHelper {
public List<ZenRule> getZenRules() {
List<ZenRule> rules = new ArrayList<>();
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return rules;
for (ZenRule rule : mConfig.automaticRules.values()) {
if (canManageAutomaticZenRule(rule)) {
@@ -327,7 +332,7 @@ public class ZenModeHelper {
public AutomaticZenRule getAutomaticZenRule(String id) {
ZenRule rule;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return null;
rule = mConfig.automaticRules.get(id);
}
@@ -364,7 +369,7 @@ public class ZenModeHelper {
}
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) {
throw new AndroidRuntimeException("Could not create rule");
}
@@ -387,7 +392,7 @@ public class ZenModeHelper {
public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule,
String reason, int callingUid, boolean fromSystemOrSystemUi) {
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return false;
if (DEBUG) {
Log.d(TAG, "updateAutomaticZenRule zenRule=" + automaticZenRule
@@ -419,7 +424,7 @@ public class ZenModeHelper {
public boolean removeAutomaticZenRule(String id, String reason, int callingUid,
boolean fromSystemOrSystemUi) {
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return false;
newConfig = mConfig.copy();
ZenRule ruleToRemove = newConfig.automaticRules.get(id);
@@ -450,7 +455,7 @@ public class ZenModeHelper {
public boolean removeAutomaticZenRules(String packageName, String reason, int callingUid,
boolean fromSystemOrSystemUi) {
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return false;
newConfig = mConfig.copy();
for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) {
@@ -467,7 +472,7 @@ public class ZenModeHelper {
public void setAutomaticZenRuleState(String id, Condition condition, int callingUid,
boolean fromSystemOrSystemUi) {
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return;
newConfig = mConfig.copy();
@@ -481,7 +486,7 @@ public class ZenModeHelper {
public void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition, int callingUid,
boolean fromSystemOrSystemUi) {
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return;
newConfig = mConfig.copy();
@@ -491,6 +496,7 @@ public class ZenModeHelper {
}
}
+ @GuardedBy("mConfigLock")
private void setAutomaticZenRuleStateLocked(ZenModeConfig config, List<ZenRule> rules,
Condition condition, int callingUid, boolean fromSystemOrSystemUi) {
if (rules == null || rules.isEmpty()) return;
@@ -538,7 +544,7 @@ public class ZenModeHelper {
return 0;
}
int count = 0;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
for (ZenRule rule : mConfig.automaticRules.values()) {
if (cn.equals(rule.component) || cn.equals(rule.configurationActivity)) {
count++;
@@ -555,7 +561,7 @@ public class ZenModeHelper {
return 0;
}
int count = 0;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
for (ZenRule rule : mConfig.automaticRules.values()) {
if (pkg.equals(rule.getPkg())) {
count++;
@@ -588,19 +594,23 @@ public class ZenModeHelper {
protected void updateDefaultZenRules(int callingUid, boolean fromSystemOrSystemUi) {
updateDefaultAutomaticRuleNames();
- for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) {
- ZenRule currRule = mConfig.automaticRules.get(defaultRule.id);
- // if default rule wasn't user-modified nor enabled, use localized name
- // instead of previous system name
- if (currRule != null && !currRule.modified && !currRule.enabled
- && !defaultRule.name.equals(currRule.name)) {
- if (canManageAutomaticZenRule(currRule)) {
- if (DEBUG) Slog.d(TAG, "Locale change - updating default zen rule name "
- + "from " + currRule.name + " to " + defaultRule.name);
- // update default rule (if locale changed, name of rule will change)
- currRule.name = defaultRule.name;
- updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule),
- "locale changed", callingUid, fromSystemOrSystemUi);
+ synchronized (mConfigLock) {
+ for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) {
+ ZenRule currRule = mConfig.automaticRules.get(defaultRule.id);
+ // if default rule wasn't user-modified nor enabled, use localized name
+ // instead of previous system name
+ if (currRule != null && !currRule.modified && !currRule.enabled
+ && !defaultRule.name.equals(currRule.name)) {
+ if (canManageAutomaticZenRule(currRule)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Locale change - updating default zen rule name "
+ + "from " + currRule.name + " to " + defaultRule.name);
+ }
+ // update default rule (if locale changed, name of rule will change)
+ currRule.name = defaultRule.name;
+ updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule),
+ "locale changed", callingUid, fromSystemOrSystemUi);
+ }
}
}
}
@@ -686,7 +696,7 @@ public class ZenModeHelper {
private void setManualZenMode(int zenMode, Uri conditionId, String reason, String caller,
boolean setRingerMode, int callingUid, boolean fromSystemOrSystemUi) {
ZenModeConfig newConfig;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig == null) return;
if (!Global.isValidZenMode(zenMode)) return;
if (DEBUG) Log.d(TAG, "setManualZenMode " + Global.zenModeToString(zenMode)
@@ -715,7 +725,7 @@ public class ZenModeHelper {
void dump(ProtoOutputStream proto) {
proto.write(ZenModeProto.ZEN_MODE, mZenMode);
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
if (mConfig.manualRule != null) {
mConfig.manualRule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS);
}
@@ -737,14 +747,14 @@ public class ZenModeHelper {
pw.println(Global.zenModeToString(mZenMode));
pw.print(prefix);
pw.println("mConsolidatedPolicy=" + mConsolidatedPolicy.toString());
- synchronized(mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
final int N = mConfigs.size();
for (int i = 0; i < N; i++) {
dump(pw, prefix, "mConfigs[u=" + mConfigs.keyAt(i) + "]", mConfigs.valueAt(i));
}
}
pw.print(prefix); pw.print("mUser="); pw.println(mUser);
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
dump(pw, prefix, "mConfig", mConfig);
}
@@ -833,7 +843,7 @@ public class ZenModeHelper {
Settings.Secure.ZEN_SETTINGS_UPDATED, 1, userId);
}
if (DEBUG) Log.d(TAG, reason);
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
setConfigLocked(config, null, reason, Process.SYSTEM_UID, true);
}
}
@@ -841,7 +851,7 @@ public class ZenModeHelper {
public void writeXml(TypedXmlSerializer out, boolean forBackup, Integer version, int userId)
throws IOException {
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
final int n = mConfigs.size();
for (int i = 0; i < n; i++) {
if (forBackup && mConfigs.keyAt(i) != userId) {
@@ -856,7 +866,9 @@ public class ZenModeHelper {
* @return user-specified default notification policy for priority only do not disturb
*/
public Policy getNotificationPolicy() {
- return getNotificationPolicy(mConfig);
+ synchronized (mConfigLock) {
+ return getNotificationPolicy(mConfig);
+ }
}
private static Policy getNotificationPolicy(ZenModeConfig config) {
@@ -867,8 +879,8 @@ public class ZenModeHelper {
* Sets the global notification policy used for priority only do not disturb
*/
public void setNotificationPolicy(Policy policy, int callingUid, boolean fromSystemOrSystemUi) {
- if (policy == null || mConfig == null) return;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
+ if (policy == null || mConfig == null) return;
final ZenModeConfig newConfig = mConfig.copy();
newConfig.applyNotificationPolicy(policy);
setConfigLocked(newConfig, null, "setNotificationPolicy", callingUid,
@@ -881,7 +893,7 @@ public class ZenModeHelper {
*/
private void cleanUpZenRules() {
long currentTime = System.currentTimeMillis();
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
final ZenModeConfig newConfig = mConfig.copy();
if (newConfig.automaticRules != null) {
for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) {
@@ -906,7 +918,7 @@ public class ZenModeHelper {
* @return a copy of the zen mode configuration
*/
public ZenModeConfig getConfig() {
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
return mConfig.copy();
}
}
@@ -918,7 +930,8 @@ public class ZenModeHelper {
return mConsolidatedPolicy.copy();
}
- public boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent,
+ @GuardedBy("mConfigLock")
+ private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent,
String reason, int callingUid, boolean fromSystemOrSystemUi) {
return setConfigLocked(config, reason, triggeringComponent, true /*setRingerMode*/,
callingUid, fromSystemOrSystemUi);
@@ -926,11 +939,12 @@ public class ZenModeHelper {
public void setConfig(ZenModeConfig config, ComponentName triggeringComponent, String reason,
int callingUid, boolean fromSystemOrSystemUi) {
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
setConfigLocked(config, triggeringComponent, reason, callingUid, fromSystemOrSystemUi);
}
}
+ @GuardedBy("mConfigLock")
private boolean setConfigLocked(ZenModeConfig config, String reason,
ComponentName triggeringComponent, boolean setRingerMode, int callingUid,
boolean fromSystemOrSystemUi) {
@@ -942,7 +956,7 @@ public class ZenModeHelper {
}
if (config.user != mUser) {
// simply store away for background users
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
mConfigs.put(config.user, config);
}
if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);
@@ -951,7 +965,7 @@ public class ZenModeHelper {
// handle CPS backed conditions - danger! may modify config
mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
mConfigs.put(config.user, config);
}
if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
@@ -979,6 +993,7 @@ public class ZenModeHelper {
* Carries out a config update (if needed) and (re-)evaluates the zen mode value afterwards.
* If logging is enabled, will also request logging of the outcome of this change if needed.
*/
+ @GuardedBy("mConfigLock")
private void updateConfigAndZenModeLocked(ZenModeConfig config, String reason,
boolean setRingerMode, int callingUid, boolean fromSystemOrSystemUi) {
final boolean logZenModeEvents = mFlagResolver.isEnabled(
@@ -993,7 +1008,7 @@ public class ZenModeHelper {
}
final String val = Integer.toString(config.hashCode());
Global.putString(mContext.getContentResolver(), Global.ZEN_MODE_CONFIG_ETAG, val);
- evaluateZenMode(reason, setRingerMode);
+ evaluateZenModeLocked(reason, setRingerMode);
// After all changes have occurred, log if requested
if (logZenModeEvents) {
ZenModeEventLogger.ZenModeInfo newInfo = new ZenModeEventLogger.ZenModeInfo(
@@ -1025,7 +1040,8 @@ public class ZenModeHelper {
}
@VisibleForTesting
- protected void evaluateZenMode(String reason, boolean setRingerMode) {
+ @GuardedBy("mConfigLock")
+ protected void evaluateZenModeLocked(String reason, boolean setRingerMode) {
if (DEBUG) Log.d(TAG, "evaluateZenMode");
if (mConfig == null) return;
final int policyHashBefore = mConsolidatedPolicy == null ? 0
@@ -1056,8 +1072,8 @@ public class ZenModeHelper {
}
private int computeZenMode() {
- if (mConfig == null) return Global.ZEN_MODE_OFF;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
+ if (mConfig == null) return Global.ZEN_MODE_OFF;
if (mConfig.manualRule != null) return mConfig.manualRule.zenMode;
int zen = Global.ZEN_MODE_OFF;
for (ZenRule automaticRule : mConfig.automaticRules.values()) {
@@ -1094,8 +1110,8 @@ public class ZenModeHelper {
}
private void updateConsolidatedPolicy(String reason) {
- if (mConfig == null) return;
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
+ if (mConfig == null) return;
ZenPolicy policy = new ZenPolicy();
if (mConfig.manualRule != null) {
applyCustomPolicy(policy, mConfig.manualRule);
@@ -1293,7 +1309,7 @@ public class ZenModeHelper {
* Generate pulled atoms about do not disturb configurations.
*/
public void pullRules(List<StatsEvent> events) {
- synchronized (mConfigsLock) {
+ synchronized (mConfigsArrayLock) {
final int numConfigs = mConfigs.size();
for (int i = 0; i < numConfigs; i++) {
final int user = mConfigs.keyAt(i);
@@ -1319,6 +1335,7 @@ public class ZenModeHelper {
}
}
+ @GuardedBy("mConfigsArrayLock")
private void ruleToProtoLocked(int user, ZenRule rule, boolean isManualRule,
List<StatsEvent> events) {
// Make the ID safe.
@@ -1389,7 +1406,7 @@ public class ZenModeHelper {
if (mZenMode == Global.ZEN_MODE_OFF
|| (mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
- && !ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(mConfig))) {
+ && !areAllPriorityOnlyRingerSoundsMuted())) {
// in priority only with ringer not muted, save ringer mode changes
// in dnd off, save ringer mode changes
setPreviousRingerModeSetting(ringerModeNew);
@@ -1410,8 +1427,7 @@ public class ZenModeHelper {
&& (mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS
|| mZenMode == Global.ZEN_MODE_ALARMS
|| (mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
- && ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(
- mConfig)))) {
+ && areAllPriorityOnlyRingerSoundsMuted()))) {
newZen = Global.ZEN_MODE_OFF;
} else if (mZenMode != Global.ZEN_MODE_OFF) {
ringerModeExternalOut = AudioManager.RINGER_MODE_SILENT;
@@ -1430,6 +1446,12 @@ public class ZenModeHelper {
return ringerModeExternalOut;
}
+ private boolean areAllPriorityOnlyRingerSoundsMuted() {
+ synchronized (mConfigLock) {
+ return ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(mConfig);
+ }
+ }
+
@Override
public int onSetRingerModeExternal(int ringerModeOld, int ringerModeNew, String caller,
int ringerModeInternal, VolumePolicy policy) {
@@ -1633,7 +1655,7 @@ public class ZenModeHelper {
private void emitRules() {
final long now = SystemClock.elapsedRealtime();
final long since = (now - mRuleCountLogTime);
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
int numZenRules = mConfig.automaticRules.size();
if (mNumZenRules != numZenRules
|| since > MINIMUM_LOG_PERIOD_MS) {
@@ -1651,7 +1673,7 @@ public class ZenModeHelper {
private void emitDndType() {
final long now = SystemClock.elapsedRealtime();
final long since = (now - mTypeLogTimeMs);
- synchronized (mConfig) {
+ synchronized (mConfigLock) {
boolean dndOn = mZenMode != Global.ZEN_MODE_OFF;
int zenType = !dndOn ? DND_OFF
: (mConfig.manualRule != null) ? DND_ON_MANUAL : DND_ON_AUTOMATIC;
diff --git a/services/core/java/com/android/server/pm/DefaultAppProvider.java b/services/core/java/com/android/server/pm/DefaultAppProvider.java
index c18d0e9ef35e..fc61451b0289 100644
--- a/services/core/java/com/android/server/pm/DefaultAppProvider.java
+++ b/services/core/java/com/android/server/pm/DefaultAppProvider.java
@@ -24,14 +24,10 @@ import android.os.Binder;
import android.os.UserHandle;
import android.util.Slog;
-import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.CollectionUtils;
import com.android.server.FgThread;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -70,27 +66,19 @@ public class DefaultAppProvider {
* Set the package name of the default browser.
*
* @param packageName package name of the default browser, or {@code null} to unset
- * @param async whether the operation should be asynchronous
* @param userId the user ID
- * @return whether the default browser was successfully set.
*/
- public boolean setDefaultBrowser(@Nullable String packageName, boolean async,
- @UserIdInt int userId) {
- if (userId == UserHandle.USER_ALL) {
- return false;
- }
+ public void setDefaultBrowser(@Nullable String packageName, @UserIdInt int userId) {
final RoleManager roleManager = mRoleManagerSupplier.get();
if (roleManager == null) {
- return false;
+ return;
}
final UserHandle user = UserHandle.of(userId);
final Executor executor = FgThread.getExecutor();
- final AndroidFuture<Void> future = new AndroidFuture<>();
final Consumer<Boolean> callback = successful -> {
- if (successful) {
- future.complete(null);
- } else {
- future.completeExceptionally(new RuntimeException());
+ if (!successful) {
+ Slog.e(PackageManagerService.TAG, "Failed to set default browser to "
+ + packageName);
}
};
final long identity = Binder.clearCallingIdentity();
@@ -102,19 +90,9 @@ public class DefaultAppProvider {
roleManager.clearRoleHoldersAsUser(RoleManager.ROLE_BROWSER, 0, user, executor,
callback);
}
- if (!async) {
- try {
- future.get(5, TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- Slog.e(PackageManagerService.TAG, "Exception while setting default browser: "
- + packageName, e);
- return false;
- }
- }
} finally {
Binder.restoreCallingIdentity(identity);
}
- return true;
}
/**
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 50f1673cae44..fbd54555dbbf 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -110,6 +110,7 @@ import android.app.backup.IBackupManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.DataLoaderType;
@@ -119,7 +120,6 @@ import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
-import android.content.pm.ResolveInfo;
import android.content.pm.SharedLibraryInfo;
import android.content.pm.Signature;
import android.content.pm.SigningDetails;
@@ -184,7 +184,9 @@ import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.SharedLibraryWrapper;
import com.android.server.pm.pkg.component.ComponentMutateUtils;
+import com.android.server.pm.pkg.component.ParsedActivity;
import com.android.server.pm.pkg.component.ParsedInstrumentation;
+import com.android.server.pm.pkg.component.ParsedIntentInfo;
import com.android.server.pm.pkg.component.ParsedPermission;
import com.android.server.pm.pkg.component.ParsedPermissionGroup;
import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
@@ -207,6 +209,7 @@ import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -3925,23 +3928,6 @@ final class InstallPackageHelper {
}
}
- // If this is a system app we hadn't seen before, and this is a first boot or OTA,
- // we need to unstop it if it doesn't have a launcher entry.
- if (mPm.mShouldStopSystemPackagesByDefault && scanResult.mRequest.mPkgSetting == null
- && ((scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0)
- && ((scanFlags & SCAN_AS_SYSTEM) != 0)) {
- final Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
- launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- launcherIntent.setPackage(parsedPackage.getPackageName());
- final List<ResolveInfo> launcherActivities =
- mPm.snapshotComputer().queryIntentActivitiesInternal(launcherIntent, null,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0);
- if (launcherActivities.isEmpty()) {
- scanResult.mPkgSetting.setStopped(false, 0);
- }
- }
-
if (mIncrementalManager != null && isIncrementalPath(parsedPackage.getPath())) {
if (scanResult.mPkgSetting != null && scanResult.mPkgSetting.isLoading()) {
// Continue monitoring loading progress of active incremental packages
@@ -4314,6 +4300,8 @@ final class InstallPackageHelper {
// - It's an APEX or overlay package since stopped state does not affect them.
// - It is enumerated with a <initial-package-state> tag having the stopped attribute
// set to false
+ // - It doesn't have an enabled and exported launcher activity, which means the user
+ // wouldn't have a way to un-stop it
final boolean isApexPkg = (scanFlags & SCAN_AS_APEX) != 0;
if (mPm.mShouldStopSystemPackagesByDefault
&& scanSystemPartition
@@ -4322,8 +4310,9 @@ final class InstallPackageHelper {
&& !parsedPackage.isOverlayIsStatic()
) {
String packageName = parsedPackage.getPackageName();
- if (!mPm.mInitialNonStoppedSystemPackages.contains(packageName)
- && !"android".contentEquals(packageName)) {
+ if (!"android".contentEquals(packageName)
+ && !mPm.mInitialNonStoppedSystemPackages.contains(packageName)
+ && hasLauncherEntry(parsedPackage)) {
scanFlags |= SCAN_AS_STOPPED_SYSTEM_APP;
}
}
@@ -4333,6 +4322,26 @@ final class InstallPackageHelper {
return new Pair<>(scanResult, shouldHideSystemApp);
}
+ private static boolean hasLauncherEntry(ParsedPackage parsedPackage) {
+ final HashSet<String> categories = new HashSet<>();
+ categories.add(Intent.CATEGORY_LAUNCHER);
+ final List<ParsedActivity> activities = parsedPackage.getActivities();
+ for (int indexActivity = 0; indexActivity < activities.size(); indexActivity++) {
+ final ParsedActivity activity = activities.get(indexActivity);
+ if (!activity.isEnabled() || !activity.isExported()) {
+ continue;
+ }
+ final List<ParsedIntentInfo> intents = activity.getIntents();
+ for (int indexIntent = 0; indexIntent < intents.size(); indexIntent++) {
+ final IntentFilter intentFilter = intents.get(indexIntent).getIntentFilter();
+ if (intentFilter != null && intentFilter.matchCategories(categories) == null) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Returns if forced apk verification can be skipped for the whole package, including splits.
*/
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 2fc22bf79d91..dbc2fd89e58c 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3535,6 +3535,18 @@ public class PackageManagerService implements PackageSender, TestUtilityService
// within these users.
mPermissionManager.restoreDelayedRuntimePermissions(packageName, userId);
+ // Restore default browser setting if it is now installed.
+ String defaultBrowser;
+ synchronized (mLock) {
+ defaultBrowser = mSettings.getPendingDefaultBrowserLPr(userId);
+ }
+ if (Objects.equals(packageName, defaultBrowser)) {
+ mDefaultAppProvider.setDefaultBrowser(packageName, userId);
+ synchronized (mLock) {
+ mSettings.removePendingDefaultBrowserLPw(userId);
+ }
+ }
+
// Persistent preferred activity might have came into effect due to this
// install.
mPreferredActivityHelper.updateDefaultHomeNotLocked(snapshotComputer(), userId);
@@ -6732,7 +6744,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService
@Override
public String removeLegacyDefaultBrowserPackageName(int userId) {
synchronized (mLock) {
- return mSettings.removeDefaultBrowserPackageNameLPw(userId);
+ return mSettings.removePendingDefaultBrowserLPw(userId);
}
}
@@ -7569,8 +7581,13 @@ public class PackageManagerService implements PackageSender, TestUtilityService
callback);
}
- void setDefaultBrowser(@Nullable String packageName, boolean async, @UserIdInt int userId) {
- mDefaultAppProvider.setDefaultBrowser(packageName, async, userId);
+ @Nullable
+ String getDefaultBrowser(@UserIdInt int userId) {
+ return mDefaultAppProvider.getDefaultBrowser(userId);
+ }
+
+ void setDefaultBrowser(@Nullable String packageName, @UserIdInt int userId) {
+ mDefaultAppProvider.setDefaultBrowser(packageName, userId);
}
PackageUsage getPackageUsage() {
diff --git a/services/core/java/com/android/server/pm/PreferredActivityHelper.java b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
index 214a8b80b35d..76e7070bd3fe 100644
--- a/services/core/java/com/android/server/pm/PreferredActivityHelper.java
+++ b/services/core/java/com/android/server/pm/PreferredActivityHelper.java
@@ -557,9 +557,8 @@ final class PreferredActivityHelper {
serializer.startDocument(null, true);
serializer.startTag(null, TAG_DEFAULT_APPS);
- synchronized (mPm.mLock) {
- mPm.mSettings.writeDefaultAppsLPr(serializer, userId);
- }
+ final String defaultBrowser = mPm.getDefaultBrowser(userId);
+ Settings.writeDefaultApps(serializer, defaultBrowser);
serializer.endTag(null, TAG_DEFAULT_APPS);
serializer.endDocument();
@@ -584,14 +583,19 @@ final class PreferredActivityHelper {
parser.setInput(new ByteArrayInputStream(backup), StandardCharsets.UTF_8.name());
restoreFromXml(parser, userId, TAG_DEFAULT_APPS,
(parser1, userId1) -> {
- final String defaultBrowser;
- synchronized (mPm.mLock) {
- mPm.mSettings.readDefaultAppsLPw(parser1, userId1);
- defaultBrowser = mPm.mSettings.removeDefaultBrowserPackageNameLPw(
- userId1);
- }
+ final String defaultBrowser = Settings.readDefaultApps(parser1);
if (defaultBrowser != null) {
- mPm.setDefaultBrowser(defaultBrowser, false, userId1);
+ final PackageStateInternal packageState = mPm.snapshotComputer()
+ .getPackageStateInternal(defaultBrowser);
+ if (packageState != null
+ && packageState.getUserStateOrDefault(userId1).isInstalled()) {
+ mPm.setDefaultBrowser(defaultBrowser, userId1);
+ } else {
+ synchronized (mPm.mLock) {
+ mPm.mSettings.setPendingDefaultBrowserLPw(defaultBrowser,
+ userId1);
+ }
+ }
}
});
} catch (Exception e) {
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 532ae718c030..677a5d11cc6b 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -517,9 +517,11 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
private final WatchedArrayMap<String, String> mRenamedPackages =
new WatchedArrayMap<String, String>();
- // For every user, it is used to find the package name of the default Browser App.
+ // For every user, it is used to find the package name of the default browser app pending to be
+ // applied, either on first boot after upgrade, or after backup & restore but before app is
+ // installed.
@Watched
- final WatchedSparseArray<String> mDefaultBrowserApp = new WatchedSparseArray<String>();
+ final WatchedSparseArray<String> mPendingDefaultBrowser = new WatchedSparseArray<>();
// TODO(b/161161364): This seems unused, and is probably not relevant in the new API, but should
// verify.
@@ -592,7 +594,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
mAppIds.registerObserver(mObserver);
mRenamedPackages.registerObserver(mObserver);
mNextAppLinkGeneration.registerObserver(mObserver);
- mDefaultBrowserApp.registerObserver(mObserver);
+ mPendingDefaultBrowser.registerObserver(mObserver);
mPendingPackages.registerObserver(mObserver);
mPastSignatures.registerObserver(mObserver);
mKeySetRefs.registerObserver(mObserver);
@@ -787,7 +789,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
mRenamedPackages.snapshot(r.mRenamedPackages);
mNextAppLinkGeneration.snapshot(r.mNextAppLinkGeneration);
- mDefaultBrowserApp.snapshot(r.mDefaultBrowserApp);
+ mPendingDefaultBrowser.snapshot(r.mPendingDefaultBrowser);
// mReadMessages
mPendingPackages = r.mPendingPackagesSnapshot.snapshot();
mPendingPackagesSnapshot = new SnapshotCache.Sealed<>();
@@ -1504,8 +1506,16 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
return cpir;
}
- String removeDefaultBrowserPackageNameLPw(int userId) {
- return (userId == UserHandle.USER_ALL) ? null : mDefaultBrowserApp.removeReturnOld(userId);
+ String getPendingDefaultBrowserLPr(int userId) {
+ return mPendingDefaultBrowser.get(userId);
+ }
+
+ void setPendingDefaultBrowserLPw(String defaultBrowser, int userId) {
+ mPendingDefaultBrowser.put(userId, defaultBrowser);
+ }
+
+ String removePendingDefaultBrowserLPw(int userId) {
+ return mPendingDefaultBrowser.removeReturnOld(userId);
}
private File getUserSystemDirectory(int userId) {
@@ -1688,6 +1698,19 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
void readDefaultAppsLPw(XmlPullParser parser, int userId)
throws XmlPullParserException, IOException {
+ String defaultBrowser = readDefaultApps(parser);
+ if (defaultBrowser != null) {
+ mPendingDefaultBrowser.put(userId, defaultBrowser);
+ }
+ }
+
+ /**
+ * @return the package name for the default browser app, or {@code null} if none.
+ */
+ @Nullable
+ static String readDefaultApps(@NonNull XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ String defaultBrowser = null;
int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -1697,8 +1720,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
}
String tagName = parser.getName();
if (tagName.equals(TAG_DEFAULT_BROWSER)) {
- String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
- mDefaultBrowserApp.put(userId, packageName);
+ defaultBrowser = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
} else if (tagName.equals(TAG_DEFAULT_DIALER)) {
// Ignored.
} else {
@@ -1708,6 +1730,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
XmlUtils.skipCurrentTag(parser);
}
}
+ return defaultBrowser;
}
void readBlockUninstallPackagesLPw(TypedXmlPullParser parser, int userId)
@@ -2087,8 +2110,13 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile
void writeDefaultAppsLPr(XmlSerializer serializer, int userId)
throws IllegalArgumentException, IllegalStateException, IOException {
+ String defaultBrowser = mPendingDefaultBrowser.get(userId);
+ writeDefaultApps(serializer, defaultBrowser);
+ }
+
+ static void writeDefaultApps(@NonNull XmlSerializer serializer, @Nullable String defaultBrowser)
+ throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(null, TAG_DEFAULT_APPS);
- String defaultBrowser = mDefaultBrowserApp.get(userId);
if (!TextUtils.isEmpty(defaultBrowser)) {
serializer.startTag(null, TAG_DEFAULT_BROWSER);
serializer.attribute(null, ATTR_PACKAGE_NAME, defaultBrowser);
diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
index 784e177d6362..69cc1252f696 100644
--- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java
+++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
@@ -16,6 +16,7 @@
package com.android.server.policy;
+import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
@@ -23,6 +24,8 @@ import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.XmlResourceParser;
+import android.hardware.input.InputManager;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.TextUtils;
@@ -30,11 +33,14 @@ import android.util.Log;
import android.util.LongSparseArray;
import android.util.Slog;
import android.util.SparseArray;
+import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import com.android.internal.policy.IShortcutService;
import com.android.internal.util.XmlUtils;
+import com.android.server.input.KeyboardMetricsCollector;
+import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -88,11 +94,13 @@ class ModifierShortcutManager {
}
private final Context mContext;
+ private final Handler mHandler;
private boolean mSearchKeyShortcutPending = false;
private boolean mConsumeSearchKeyUp = true;
- ModifierShortcutManager(Context context) {
+ ModifierShortcutManager(Context context, Handler handler) {
mContext = context;
+ mHandler = handler;
loadShortcuts();
}
@@ -273,11 +281,13 @@ class ModifierShortcutManager {
* Handle the shortcut to {@link Intent}
*
* @param kcm the {@link KeyCharacterMap} associated with the keyboard device.
- * @param keyCode The key code of the event.
+ * @param keyEvent The key event.
* @param metaState The meta key modifier state.
* @return True if invoked the shortcut, otherwise false.
*/
- private boolean handleIntentShortcut(KeyCharacterMap kcm, int keyCode, int metaState) {
+ @SuppressLint("MissingPermission")
+ private boolean handleIntentShortcut(KeyCharacterMap kcm, KeyEvent keyEvent, int metaState) {
+ final int keyCode = keyEvent.getKeyCode();
// Shortcuts are invoked through Search+key, so intercept those here
// Any printing key that is chorded with Search should be consumed
// even if no shortcut was invoked. This prevents text from being
@@ -307,6 +317,7 @@ class ModifierShortcutManager {
+ "keyCode=" + KeyEvent.keyCodeToString(keyCode) + ","
+ " category=" + category);
}
+ logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(intent));
return true;
} else {
return false;
@@ -323,11 +334,24 @@ class ModifierShortcutManager {
+ "the activity to which it is registered was not found: "
+ "META+ or SEARCH" + KeyEvent.keyCodeToString(keyCode));
}
+ logKeyboardShortcut(keyEvent, KeyboardLogEvent.getLogEventFromIntent(shortcutIntent));
return true;
}
return false;
}
+ private void logKeyboardShortcut(KeyEvent event, KeyboardLogEvent logEvent) {
+ mHandler.post(() -> handleKeyboardLogging(event, logEvent));
+ }
+
+ private void handleKeyboardLogging(KeyEvent event, KeyboardLogEvent logEvent) {
+ final InputManager inputManager = mContext.getSystemService(InputManager.class);
+ final InputDevice inputDevice = inputManager != null
+ ? inputManager.getInputDevice(event.getDeviceId()) : null;
+ KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice,
+ logEvent, event.getMetaState(), event.getKeyCode());
+ }
+
/**
* Handle the shortcut from {@link KeyEvent}
*
@@ -360,7 +384,7 @@ class ModifierShortcutManager {
}
final KeyCharacterMap kcm = event.getKeyCharacterMap();
- if (handleIntentShortcut(kcm, keyCode, metaState)) {
+ if (handleIntentShortcut(kcm, event, metaState)) {
return true;
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 5cfbcaafe82e..ca647929c271 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -219,6 +219,7 @@ import com.android.server.UiThread;
import com.android.server.display.BrightnessUtils;
import com.android.server.input.InputManagerInternal;
import com.android.server.input.KeyboardMetricsCollector;
+import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule;
@@ -409,6 +410,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
ActivityManagerInternal mActivityManagerInternal;
ActivityTaskManagerInternal mActivityTaskManagerInternal;
AutofillManagerInternal mAutofillManagerInternal;
+ InputManager mInputManager;
InputManagerInternal mInputManagerInternal;
DreamManagerInternal mDreamManagerInternal;
PowerManagerInternal mPowerManagerInternal;
@@ -762,7 +764,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
break;
case MSG_LOG_KEYBOARD_SYSTEM_EVENT:
- handleKeyboardSystemEvent(msg.arg2, (KeyEvent) msg.obj);
+ handleKeyboardSystemEvent(KeyboardLogEvent.from(msg.arg1), (KeyEvent) msg.obj);
break;
}
}
@@ -1762,8 +1764,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
private void launchAllAppsViaA11y() {
- getAccessibilityManagerInternal().performSystemAction(
- AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
+ AccessibilityManagerInternal accessibilityManager = getAccessibilityManagerInternal();
+ if (accessibilityManager != null) {
+ accessibilityManager.performSystemAction(
+ AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
+ }
}
private void toggleNotificationPanel() {
@@ -1834,6 +1839,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// If we have released the home key, and didn't do anything else
// while it was pressed, then it is time to go home!
if (!down) {
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.HOME);
if (mDisplayId == DEFAULT_DISPLAY) {
cancelPreloadRecentApps();
}
@@ -2035,6 +2041,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+ mInputManager = mContext.getSystemService(InputManager.class);
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class);
mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
@@ -2104,7 +2111,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mWakeGestureListener = new MyWakeGestureListener(mContext, mHandler);
mSettingsObserver = new SettingsObserver(mHandler);
mSettingsObserver.observe();
- mModifierShortcutManager = new ModifierShortcutManager(mContext);
+ mModifierShortcutManager = new ModifierShortcutManager(mContext, mHandler);
mUiMode = mContext.getResources().getInteger(
com.android.internal.R.integer.config_defaultUiModeType);
mHomeIntent = new Intent(Intent.ACTION_MAIN, null);
@@ -2930,20 +2937,33 @@ public class PhoneWindowManager implements WindowManagerPolicy {
* We won't log keyboard events when the input device is null
* or when it is virtual.
*/
- private void handleKeyboardSystemEvent(int keyboardSystemEvent, KeyEvent event) {
- final InputManager inputManager = mContext.getSystemService(InputManager.class);
- final InputDevice inputDevice = inputManager != null
- ? inputManager.getInputDevice(event.getDeviceId()) : null;
- if (inputDevice != null && !inputDevice.isVirtual()) {
- KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(
- inputDevice, keyboardSystemEvent,
- new int[]{event.getKeyCode()}, event.getMetaState());
+ private void handleKeyboardSystemEvent(KeyboardLogEvent keyboardLogEvent, KeyEvent event) {
+ final InputDevice inputDevice = mInputManager.getInputDevice(event.getDeviceId());
+ KeyboardMetricsCollector.logKeyboardSystemsEventReportedAtom(inputDevice,
+ keyboardLogEvent, event.getMetaState(), event.getKeyCode());
+ event.recycle();
+ }
+
+ private void logKeyboardSystemsEventOnActionUp(KeyEvent event,
+ KeyboardLogEvent keyboardSystemEvent) {
+ if (event.getAction() != KeyEvent.ACTION_UP) {
+ return;
}
+ logKeyboardSystemsEvent(event, keyboardSystemEvent);
}
- private void logKeyboardSystemsEvent(KeyEvent event, int keyboardSystemEvent) {
- mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_EVENT, 0, keyboardSystemEvent, event)
- .sendToTarget();
+ private void logKeyboardSystemsEventOnActionDown(KeyEvent event,
+ KeyboardLogEvent keyboardSystemEvent) {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return;
+ }
+ logKeyboardSystemsEvent(event, keyboardSystemEvent);
+ }
+
+ private void logKeyboardSystemsEvent(KeyEvent event, KeyboardLogEvent keyboardSystemEvent) {
+ KeyEvent eventToLog = KeyEvent.obtain(event);
+ mHandler.obtainMessage(MSG_LOG_KEYBOARD_SYSTEM_EVENT, keyboardSystemEvent.getIntValue(), 0,
+ eventToLog).sendToTarget();
}
// TODO(b/117479243): handle it in InputPolicy
@@ -3044,8 +3064,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
switch (keyCode) {
case KeyEvent.KEYCODE_HOME:
- logKeyboardSystemsEvent(event,
- FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__HOME);
return handleHomeShortcuts(displayId, focusedToken, event);
case KeyEvent.KEYCODE_MENU:
// Hijack modified menu keys for debugging features
@@ -3056,14 +3074,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
Intent intent = new Intent(Intent.ACTION_BUG_REPORT);
mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT,
null, null, null, 0, null, null);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TRIGGER_BUG_REPORT);
return true;
}
break;
case KeyEvent.KEYCODE_RECENT_APPS:
if (firstDown) {
showRecentApps(false /* triggeredFromAltTab */);
- logKeyboardSystemsEvent(event,
- FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RECENT_APPS);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS);
}
return true;
case KeyEvent.KEYCODE_APP_SWITCH:
@@ -3072,6 +3090,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
preloadRecentApps();
} else if (!down) {
toggleRecentApps();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.APP_SWITCH);
}
}
return true;
@@ -3080,6 +3099,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
launchAssistAction(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD,
deviceId, event.getEventTime(),
AssistUtils.INVOCATION_TYPE_UNKNOWN);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT);
return true;
}
break;
@@ -3092,12 +3112,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_I:
if (firstDown && event.isMetaPressed()) {
showSystemSettings();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS);
return true;
}
break;
case KeyEvent.KEYCODE_L:
if (firstDown && event.isMetaPressed()) {
lockNow(null /* options */);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LOCK_SCREEN);
return true;
}
break;
@@ -3105,8 +3127,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (firstDown && event.isMetaPressed()) {
if (event.isCtrlPressed()) {
sendSystemKeyToStatusBarAsync(event);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.OPEN_NOTES);
} else {
toggleNotificationPanel();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL);
}
return true;
}
@@ -3114,12 +3138,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_S:
if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TAKE_SCREENSHOT);
return true;
}
break;
case KeyEvent.KEYCODE_T:
if (firstDown && event.isMetaPressed()) {
toggleTaskbar();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_TASKBAR);
return true;
}
break;
@@ -3128,6 +3154,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
if (statusbar != null) {
statusbar.goToFullscreenFromSplit();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION);
return true;
}
}
@@ -3135,18 +3162,21 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
enterStageSplitFromRunningApp(true /* leftOrTop */);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) {
enterStageSplitFromRunningApp(false /* leftOrTop */);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION);
return true;
}
break;
case KeyEvent.KEYCODE_SLASH:
if (firstDown && event.isMetaPressed() && !keyguardOn) {
toggleKeyboardShortcutsMenu(event.getDeviceId());
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.OPEN_SHORTCUT_HELPER);
return true;
}
break;
@@ -3215,20 +3245,26 @@ public class PhoneWindowManager implements WindowManagerPolicy {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
intent.putExtra(EXTRA_FROM_BRIGHTNESS_KEY, true);
startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.getBrightnessEvent(keyCode));
}
return true;
case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN:
if (down) {
mInputManagerInternal.decrementKeyboardBacklight(event.getDeviceId());
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_DOWN);
}
return true;
case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP:
if (down) {
mInputManagerInternal.incrementKeyboardBacklight(event.getDeviceId());
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_UP);
}
return true;
case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE:
// TODO: Add logic
+ if (!down) {
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.KEYBOARD_BACKLIGHT_TOGGLE);
+ }
return true;
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
@@ -3254,6 +3290,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (firstDown && !keyguardOn && isUserSetupComplete()) {
if (event.isMetaPressed()) {
showRecentApps(false);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS);
return true;
} else if (mRecentAppsHeldModifiers == 0) {
final int shiftlessModifiers =
@@ -3262,6 +3299,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
shiftlessModifiers, KeyEvent.META_ALT_ON)) {
mRecentAppsHeldModifiers = shiftlessModifiers;
showRecentApps(true);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.RECENT_APPS);
return true;
}
}
@@ -3273,11 +3311,13 @@ public class PhoneWindowManager implements WindowManagerPolicy {
Message msg = mHandler.obtainMessage(MSG_HANDLE_ALL_APPS);
msg.setAsynchronous(true);
msg.sendToTarget();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.ALL_APPS);
}
return true;
case KeyEvent.KEYCODE_NOTIFICATION:
if (!down) {
toggleNotificationPanel();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL);
}
return true;
case KeyEvent.KEYCODE_SEARCH:
@@ -3285,6 +3325,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
switch (mSearchKeyBehavior) {
case SEARCH_BEHAVIOR_TARGET_ACTIVITY: {
launchTargetSearchActivity();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SEARCH);
return true;
}
case SEARCH_BEHAVIOR_DEFAULT_SEARCH:
@@ -3297,6 +3338,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (firstDown) {
int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
sendSwitchKeyboardLayout(event, direction);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LANGUAGE_SWITCH);
return true;
}
break;
@@ -3305,6 +3347,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (firstDown && event.isMetaPressed()) {
int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
sendSwitchKeyboardLayout(event, direction);
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LANGUAGE_SWITCH);
return true;
}
break;
@@ -3323,9 +3366,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (mPendingCapsLockToggle) {
mInputManagerInternal.toggleCapsLock(event.getDeviceId());
mPendingCapsLockToggle = false;
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK);
} else if (mPendingMetaAction) {
if (!canceled) {
launchAllAppsViaA11y();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.ACCESSIBILITY_ALL_APPS);
}
mPendingMetaAction = false;
}
@@ -3353,10 +3398,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (mPendingCapsLockToggle) {
mInputManagerInternal.toggleCapsLock(event.getDeviceId());
mPendingCapsLockToggle = false;
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK);
return true;
}
}
break;
+ case KeyEvent.KEYCODE_CAPS_LOCK:
+ if (!down) {
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_CAPS_LOCK);
+ }
+ break;
case KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY:
case KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY:
case KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY:
@@ -4200,6 +4251,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// Handle special keys.
switch (keyCode) {
case KeyEvent.KEYCODE_BACK: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.BACK);
if (down) {
mBackKeyHandled = false;
} else {
@@ -4217,6 +4269,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
+ logKeyboardSystemsEventOnActionDown(event,
+ KeyboardLogEvent.getVolumeEvent(keyCode));
if (down) {
sendSystemKeyToStatusBarAsync(event);
@@ -4317,6 +4371,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
case KeyEvent.KEYCODE_TV_POWER: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.TOGGLE_POWER);
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false; // wake-up will be handled separately
if (down && hdmiControlManager != null) {
@@ -4326,6 +4381,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
case KeyEvent.KEYCODE_POWER: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.TOGGLE_POWER);
EventLogTags.writeInterceptPower(
KeyEvent.actionToString(event.getAction()),
mPowerKeyHandled ? 1 : 0,
@@ -4348,12 +4404,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT:
// fall through
case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SYSTEM_NAVIGATION);
result &= ~ACTION_PASS_TO_USER;
interceptSystemNavigationKey(event);
break;
}
case KeyEvent.KEYCODE_SLEEP: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SLEEP);
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false;
if (!mPowerManager.isInteractive()) {
@@ -4368,6 +4426,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
case KeyEvent.KEYCODE_SOFT_SLEEP: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.SLEEP);
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false;
if (!down) {
@@ -4377,6 +4436,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
case KeyEvent.KEYCODE_WAKEUP: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.WAKEUP);
result &= ~ACTION_PASS_TO_USER;
isWakeKey = true;
break;
@@ -4385,6 +4445,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_MUTE:
result &= ~ACTION_PASS_TO_USER;
if (down && event.getRepeatCount() == 0) {
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.SYSTEM_MUTE);
toggleMicrophoneMuteFromKey();
}
break;
@@ -4399,6 +4460,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
case KeyEvent.KEYCODE_MEDIA_RECORD:
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
+ logKeyboardSystemsEventOnActionUp(event, KeyboardLogEvent.MEDIA_KEY);
if (MediaSessionLegacyHelper.getHelper(mContext).isGlobalPriorityActive()) {
// If the global session is active pass all media keys to it
// instead of the active window.
@@ -4443,6 +4505,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
0 /* unused */, event.getEventTime() /* eventTime */);
msg.setAsynchronous(true);
msg.sendToTarget();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_ASSISTANT);
}
result &= ~ACTION_PASS_TO_USER;
break;
@@ -4453,6 +4516,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
Message msg = mHandler.obtainMessage(MSG_LAUNCH_VOICE_ASSIST_WITH_WAKE_LOCK);
msg.setAsynchronous(true);
msg.sendToTarget();
+ logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_VOICE_ASSISTANT);
}
result &= ~ACTION_PASS_TO_USER;
break;
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index a53b831d55c1..d82f7a56a830 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -92,6 +92,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.os.WorkSource;
import android.os.WorkSource.WorkChain;
+import android.provider.DeviceConfigInterface;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.service.dreams.DreamManagerInternal;
@@ -128,6 +129,7 @@ import com.android.server.UiThread;
import com.android.server.UserspaceRebootLogger;
import com.android.server.Watchdog;
import com.android.server.am.BatteryStatsService;
+import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
import com.android.server.policy.WindowManagerPolicy;
@@ -323,6 +325,9 @@ public final class PowerManagerService extends SystemService
private final Injector mInjector;
private final PermissionCheckerWrapper mPermissionCheckerWrapper;
private final PowerPropertiesWrapper mPowerPropertiesWrapper;
+ private final DeviceConfigParameterProvider mDeviceConfigProvider;
+
+ private boolean mDisableScreenWakeLocksWhileCached;
private LightsManager mLightsManager;
private BatteryManagerInternal mBatteryManagerInternal;
@@ -1065,6 +1070,10 @@ public final class PowerManagerService extends SystemService
}
};
}
+
+ DeviceConfigParameterProvider createDeviceConfigParameterProvider() {
+ return new DeviceConfigParameterProvider(DeviceConfigInterface.REAL);
+ }
}
/** Interface for checking an app op permission */
@@ -1161,6 +1170,7 @@ public final class PowerManagerService extends SystemService
mInjector.createInattentiveSleepWarningController();
mPermissionCheckerWrapper = mInjector.createPermissionCheckerWrapper();
mPowerPropertiesWrapper = mInjector.createPowerPropertiesWrapper();
+ mDeviceConfigProvider = mInjector.createDeviceConfigParameterProvider();
mPowerGroupWakefulnessChangeListener = new PowerGroupWakefulnessChangeListener();
@@ -1346,6 +1356,14 @@ public final class PowerManagerService extends SystemService
mLightsManager = getLocalService(LightsManager.class);
mAttentionLight = mLightsManager.getLight(LightsManager.LIGHT_ID_ATTENTION);
+ updateDeviceConfigLocked();
+ mDeviceConfigProvider.addOnPropertiesChangedListener(BackgroundThread.getExecutor(),
+ properties -> {
+ synchronized (mLock) {
+ updateDeviceConfigLocked();
+ updateWakeLockDisabledStatesLocked();
+ }
+ });
// Initialize display power management.
mDisplayManagerInternal.initPowerManagement(
@@ -1545,6 +1563,12 @@ public final class PowerManagerService extends SystemService
updatePowerStateLocked();
}
+ @GuardedBy("mLock")
+ private void updateDeviceConfigLocked() {
+ mDisableScreenWakeLocksWhileCached = mDeviceConfigProvider
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ }
+
@RequiresPermission(value = android.Manifest.permission.TURN_SCREEN_ON, conditional = true)
private void acquireWakeLockInternal(IBinder lock, int displayId, int flags, String tag,
String packageName, WorkSource ws, String historyTag, int uid, int pid,
@@ -2760,13 +2784,13 @@ public final class PowerManagerService extends SystemService
/** Get wake lock summary flags that correspond to the given wake lock. */
@SuppressWarnings("deprecation")
private int getWakeLockSummaryFlags(WakeLock wakeLock) {
+ if (wakeLock.mDisabled) {
+ // We only respect this if the wake lock is not disabled.
+ return 0;
+ }
switch (wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK) {
case PowerManager.PARTIAL_WAKE_LOCK:
- if (!wakeLock.mDisabled) {
- // We only respect this if the wake lock is not disabled.
- return WAKE_LOCK_CPU;
- }
- break;
+ return WAKE_LOCK_CPU;
case PowerManager.FULL_WAKE_LOCK:
return WAKE_LOCK_SCREEN_BRIGHT | WAKE_LOCK_BUTTON_BRIGHT;
case PowerManager.SCREEN_BRIGHT_WAKE_LOCK:
@@ -4151,7 +4175,7 @@ public final class PowerManagerService extends SystemService
for (int i = 0; i < numWakeLocks; i++) {
final WakeLock wakeLock = mWakeLocks.get(i);
if ((wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK)
- == PowerManager.PARTIAL_WAKE_LOCK) {
+ == PowerManager.PARTIAL_WAKE_LOCK || isScreenLock(wakeLock)) {
if (setWakeLockDisabledStateLocked(wakeLock)) {
changed = true;
if (wakeLock.mDisabled) {
@@ -4205,6 +4229,22 @@ public final class PowerManagerService extends SystemService
}
}
return wakeLock.setDisabled(disabled);
+ } else if (mDisableScreenWakeLocksWhileCached && isScreenLock(wakeLock)) {
+ boolean disabled = false;
+ final int appid = UserHandle.getAppId(wakeLock.mOwnerUid);
+ final UidState state = wakeLock.mUidState;
+ // Cached inactive processes are never allowed to hold wake locks.
+ if (mConstants.NO_CACHED_WAKE_LOCKS
+ && appid >= Process.FIRST_APPLICATION_UID
+ && !state.mActive
+ && state.mProcState != ActivityManager.PROCESS_STATE_NONEXISTENT
+ && state.mProcState >= ActivityManager.PROCESS_STATE_TOP_SLEEPING) {
+ if (DEBUG_SPEW) {
+ Slog.d(TAG, "disabling full wakelock " + wakeLock);
+ }
+ disabled = true;
+ }
+ return wakeLock.setDisabled(disabled);
}
return false;
}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 27329e20bc8d..6821c40ec5d3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -16244,7 +16244,7 @@ public class BatteryStatsImpl extends BatteryStats {
}
NP = in.readInt();
- if (NP > 1000) {
+ if (NP > 10000) {
throw new ParcelFormatException("File corrupt: too many processes " + NP);
}
for (int ip = 0; ip < NP; ip++) {
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
index d8e6c262359d..d770792dffec 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsDataStorage.java
@@ -17,6 +17,7 @@
package com.android.server.powerstats;
import android.content.Context;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.util.FileRotator;
@@ -27,6 +28,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
+import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -266,4 +268,51 @@ public class PowerStatsDataStorage {
mLock.unlock();
}
}
+
+ /**
+ * Dump stats about stored data.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ mLock.lock();
+ try {
+ final int versionDot = mDataStorageFilename.lastIndexOf('.');
+ final String beforeVersionDot = mDataStorageFilename.substring(0, versionDot);
+ final File[] files = mDataStorageDir.listFiles();
+
+ int number = 0;
+ int dataSize = 0;
+ long earliestLogEpochTime = Long.MAX_VALUE;
+ for (int i = 0; i < files.length; i++) {
+ // Check that the stems before the version match.
+ final File file = files[i];
+ final String fileName = file.getName();
+ if (files[i].getName().startsWith(beforeVersionDot)) {
+ number++;
+ dataSize += file.length();
+ final int firstTimeChar = fileName.lastIndexOf('.') + 1;
+ final int endChar = fileName.lastIndexOf('-');
+ try {
+ final Long startTime =
+ Long.parseLong(fileName.substring(firstTimeChar, endChar));
+ if (startTime != null && startTime < earliestLogEpochTime) {
+ earliestLogEpochTime = startTime;
+ }
+ } catch (NumberFormatException nfe) {
+ Slog.e(TAG,
+ "Failed to extract start time from file : " + fileName, nfe);
+ }
+ }
+ }
+
+ if (earliestLogEpochTime != Long.MAX_VALUE) {
+ ipw.println("Earliest data time : " + new Date(earliestLogEpochTime));
+ } else {
+ ipw.println("Failed to parse earliest data time!!!");
+ }
+ ipw.println("# files : " + number);
+ ipw.println("Total data size (B) : " + dataSize);
+ } finally {
+ mLock.unlock();
+ }
+ }
}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
index 39ead13b03fe..e80a86d73f90 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsLogger.java
@@ -30,6 +30,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
@@ -354,4 +355,23 @@ public final class PowerStatsLogger extends Handler {
updateCacheFile(residencyCacheFilename, powerEntityBytes);
}
}
+
+ /**
+ * Dump stats about stored data.
+ */
+ public void dump(IndentingPrintWriter ipw) {
+ ipw.println("PowerStats Meter Data:");
+ ipw.increaseIndent();
+ mPowerStatsMeterStorage.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.println("PowerStats Model Data:");
+ ipw.increaseIndent();
+ mPowerStatsModelStorage.dump(ipw);
+ ipw.decreaseIndent();
+ ipw.println("PowerStats State Residency Data:");
+ ipw.increaseIndent();
+ mPowerStatsResidencyStorage.dump(ipw);
+ ipw.decreaseIndent();
+ }
+
}
diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java
index 2638f34fe7df..ffc9a0178971 100644
--- a/services/core/java/com/android/server/powerstats/PowerStatsService.java
+++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java
@@ -31,6 +31,7 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.UserHandle;
import android.power.PowerStatsInternal;
+import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
@@ -176,18 +177,31 @@ public class PowerStatsService extends SystemService {
} else if ("residency".equals(args[1])) {
mPowerStatsLogger.writeResidencyDataToFile(fd);
}
- } else if (args.length == 0) {
- pw.println("PowerStatsService dumpsys: available PowerEntities");
+ } else {
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+ ipw.println("PowerStatsService dumpsys: available PowerEntities");
PowerEntity[] powerEntity = getPowerStatsHal().getPowerEntityInfo();
- PowerEntityUtils.dumpsys(powerEntity, pw);
+ ipw.increaseIndent();
+ PowerEntityUtils.dumpsys(powerEntity, ipw);
+ ipw.decreaseIndent();
- pw.println("PowerStatsService dumpsys: available Channels");
+ ipw.println("PowerStatsService dumpsys: available Channels");
Channel[] channel = getPowerStatsHal().getEnergyMeterInfo();
- ChannelUtils.dumpsys(channel, pw);
+ ipw.increaseIndent();
+ ChannelUtils.dumpsys(channel, ipw);
+ ipw.decreaseIndent();
- pw.println("PowerStatsService dumpsys: available EnergyConsumers");
+ ipw.println("PowerStatsService dumpsys: available EnergyConsumers");
EnergyConsumer[] energyConsumer = getPowerStatsHal().getEnergyConsumerInfo();
- EnergyConsumerUtils.dumpsys(energyConsumer, pw);
+ ipw.increaseIndent();
+ EnergyConsumerUtils.dumpsys(energyConsumer, ipw);
+ ipw.decreaseIndent();
+
+ ipw.println("PowerStatsService dumpsys: PowerStatsLogger stats");
+ ipw.increaseIndent();
+ mPowerStatsLogger.dump(ipw);
+ ipw.decreaseIndent();
+
}
}
}
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index efd8b6d9a943..a5123311d499 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -140,6 +140,20 @@ public interface StatusBarManagerInternal {
boolean showShutdownUi(boolean isReboot, String requestString);
/**
+ * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed
+ * status should be saved without user clicking on the button. This could happen when a user
+ * swipe on the edge with the confirmation prompt showing.
+ */
+ void confirmImmersivePrompt();
+
+ /**
+ * Notify System UI that the system get into or exit immersive mode.
+ * @param rootDisplayAreaId The changed display area Id.
+ * @param isImmersiveMode {@code true} if the display area get into immersive mode.
+ */
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+
+ /**
* Show a rotation suggestion that a user may approve to rotate the screen.
*
* @param rotation rotation suggestion
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index cc849b6fbf91..40e9c1305f01 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -27,6 +27,7 @@ import static android.app.StatusBarManager.NavBarMode;
import static android.app.StatusBarManager.SessionFlags;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
import android.Manifest;
@@ -638,6 +639,31 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
return false;
}
+ @Override
+ public void confirmImmersivePrompt() {
+ if (mBar == null) {
+ return;
+ }
+ try {
+ mBar.confirmImmersivePrompt();
+ } catch (RemoteException ex) {
+ }
+ }
+
+ @Override
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ if (mBar == null) {
+ return;
+ }
+ if (!CLIENT_TRANSIENT) {
+ // Only call from here when the client transient is not enabled.
+ try {
+ mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
// TODO(b/118592525): support it per display if necessary.
@Override
public void onProposedRotationChanged(int rotation, boolean isValid) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index ee7dc5007d97..309a9c0e0372 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2895,6 +2895,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
checkPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT);
final long ident = Binder.clearCallingIdentity();
try {
+ List<WallpaperData> pendingColorExtraction = new ArrayList<>();
synchronized (mLock) {
WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId);
WallpaperData lockWallpaper = mLockWallpaperMap.get(mCurrentUserId);
@@ -2930,7 +2931,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
// Need to extract colors again to re-calculate dark hints after
// applying dimming.
wp.mIsColorExtractedFromDim = true;
- notifyWallpaperColorsChanged(wp, wp.mWhich);
+ pendingColorExtraction.add(wp);
changed = true;
}
}
@@ -2962,6 +2963,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
}
+ for (WallpaperData wp: pendingColorExtraction) {
+ notifyWallpaperColorsChanged(wp, wp.mWhich);
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index c5f63ced989c..a6d5c19395b0 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -285,9 +285,9 @@ class ActivityMetricsLogger {
final LaunchingState mLaunchingState;
/** The type can be cold (new process), warm (new activity), or hot (bring to front). */
- final int mTransitionType;
+ int mTransitionType;
/** Whether the process was already running when the transition started. */
- final boolean mProcessRunning;
+ boolean mProcessRunning;
/** whether the process of the launching activity didn't have any active activity. */
final boolean mProcessSwitch;
/** The process state of the launching activity prior to the launch */
@@ -972,6 +972,19 @@ class ActivityMetricsLogger {
// App isn't attached to record yet, so match with info.
if (info.mLastLaunchedActivity.info.applicationInfo == appInfo) {
info.mBindApplicationDelayMs = info.calculateCurrentDelay();
+ if (info.mProcessRunning) {
+ // It was HOT/WARM launch, but the process was died somehow right after the
+ // launch request.
+ info.mProcessRunning = false;
+ info.mTransitionType = TYPE_TRANSITION_COLD_LAUNCH;
+ final String msg = "Process " + info.mLastLaunchedActivity.info.processName
+ + " restarted";
+ Slog.i(TAG, msg);
+ if (info.mLaunchingState.mTraceName != null) {
+ Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, msg + "#"
+ + LaunchingState.sTraceSeqId);
+ }
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index dd6bcb1060ea..e93f35802953 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -4516,7 +4516,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
mTransitionChangeFlags |= FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
}
// Post cleanup after the visibility and animation are transferred.
- fromActivity.postWindowRemoveStartingWindowCleanup(tStartingWindow);
+ fromActivity.postWindowRemoveStartingWindowCleanup();
fromActivity.mVisibleSetFromTransferredStartingWindow = false;
mWmService.updateFocusedWindowLocked(
@@ -7443,27 +7443,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
}
}
- void postWindowRemoveStartingWindowCleanup(WindowState win) {
- // TODO: Something smells about the code below...Is there a better way?
- if (mStartingWindow == win) {
- ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Notify removed startingWindow %s", win);
- removeStartingWindow();
- } else if (mChildren.size() == 0) {
- // If this is the last window and we had requested a starting transition window,
- // well there is no point now.
- ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Nulling last startingData");
- mStartingData = null;
- if (mVisibleSetFromTransferredStartingWindow) {
- // We set the visible state to true for the token from a transferred starting
- // window. We now reset it back to false since the starting window was the last
- // window in the token.
- setVisible(false);
- }
- } else if (mChildren.size() == 1 && mStartingSurface != null && !isRelaunching()) {
- // If this is the last window except for a starting transition window,
- // we need to get rid of the starting transition.
- ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Last window, removing starting window %s", win);
- removeStartingWindow();
+ void postWindowRemoveStartingWindowCleanup() {
+ if (mChildren.size() == 0 && mVisibleSetFromTransferredStartingWindow) {
+ // We set the visible state to true for the token from a transferred starting
+ // window. We now reset it back to false since the starting window was the last
+ // window in the token.
+ setVisible(false);
}
}
@@ -7994,6 +7979,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
mLastReportedConfiguration.getMergedConfiguration())) {
ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */,
false /* ignoreVisibility */, true /* isRequestedOrientationChanged */);
+ if (mTransitionController.inPlayingTransition(this)) {
+ mTransitionController.mValidateActivityCompat.add(this);
+ }
}
mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged(
@@ -9413,6 +9401,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A
if (info.applicationInfo == null) {
return info.getMinAspectRatio();
}
+ if (mLetterboxUiController.shouldApplyUserMinAspectRatioOverride()) {
+ return mLetterboxUiController.getUserMinAspectRatio();
+ }
if (!mLetterboxUiController.shouldOverrideMinAspectRatio()) {
return info.getMinAspectRatio();
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 884100c5da33..3c650e32beb9 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2032,6 +2032,13 @@ class ActivityStarter {
}
// ASM rules have failed. Log why
+ return logAsmFailureAndCheckFeatureEnabled(r, newTask, targetTask, shouldBlockActivityStart,
+ taskToFront);
+ }
+
+ private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord r, boolean newTask,
+ Task targetTask, boolean shouldBlockActivityStart, boolean taskToFront) {
+ // ASM rules have failed. Log why
ActivityRecord targetTopActivity = targetTask == null ? null
: targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop());
@@ -2041,6 +2048,13 @@ class ActivityStarter {
? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK
: FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK);
+ boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags
+ .shouldRestrictActivitySwitch(mCallingUid)
+ && shouldBlockActivityStart;
+
+ String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", r, targetTask,
+ targetTopActivity, blockActivityStartAndFeatureEnabled, /*taskToFront*/taskToFront);
+
FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
/* caller_uid */
mSourceRecord != null ? mSourceRecord.getUid() : mCallingUid,
@@ -2069,24 +2083,21 @@ class ActivityStarter {
targetTask != null && mSourceRecord != null
&& !targetTask.equals(mSourceRecord.getTask()) && targetTask.isVisible(),
/* bal_code */
- mBalCode
+ mBalCode,
+ /* task_stack */
+ asmDebugInfo
);
- boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags
- .shouldRestrictActivitySwitch(mCallingUid)
- && shouldBlockActivityStart;
-
String launchedFromPackageName = r.launchedFromPackage;
if (ActivitySecurityModelFeatureFlags.shouldShowToast(mCallingUid)) {
String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
+ (blockActivityStartAndFeatureEnabled ? " blocked " : " would block ")
+ getApplicationLabel(mService.mContext.getPackageManager(),
- launchedFromPackageName);
+ launchedFromPackageName);
UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
toastText, Toast.LENGTH_LONG).show());
- logDebugInfoForActivitySecurity("Launch", r, targetTask, targetTopActivity,
- blockActivityStartAndFeatureEnabled, /* taskToFront */ taskToFront);
+ Slog.i(TAG, asmDebugInfo);
}
if (blockActivityStartAndFeatureEnabled) {
@@ -2104,7 +2115,7 @@ class ActivityStarter {
}
/** Only called when an activity launch may be blocked, which should happen very rarely */
- private void logDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask,
+ private String getDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask,
ActivityRecord targetTopActivity, boolean blockActivityStartAndFeatureEnabled,
boolean taskToFront) {
final String prefix = "[ASM] ";
@@ -2165,7 +2176,7 @@ class ActivityStarter {
joiner.add(prefix + "BalCode: " + balCodeToString(mBalCode));
joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------");
- Slog.i(TAG, joiner.toString());
+ return joiner.toString();
}
/**
@@ -2339,7 +2350,7 @@ class ActivityStarter {
+ ActivitySecurityModelFeatureFlags.DOC_LINK,
Toast.LENGTH_LONG).show());
- logDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop,
+ getDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop,
shouldBlockActivityStart, /* taskToFront */ true);
}
}
@@ -3100,7 +3111,18 @@ class ActivityStarter {
} else {
TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
if (candidateTf == null) {
- final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */);
+ // Puts the activity on the top-most non-isolated navigation TF, unless the
+ // activity is launched from the same TF.
+ final TaskFragment sourceTaskFragment =
+ mSourceRecord != null ? mSourceRecord.getTaskFragment() : null;
+ final ActivityRecord top = task.getActivity(r -> {
+ if (!r.canBeTopRunning()) {
+ return false;
+ }
+ final TaskFragment taskFragment = r.getTaskFragment();
+ return !taskFragment.isIsolatedNav() || (sourceTaskFragment != null
+ && sourceTaskFragment == taskFragment);
+ });
if (top != null) {
candidateTf = top.getTaskFragment();
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 50948e1cdec4..cc130c407690 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1752,7 +1752,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
/* multi_window */
false,
/* bal_code */
- -1
+ -1,
+ /* task_stack */
+ null
);
boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
@@ -2201,7 +2203,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
displaySwapping |= s.isDisplaySleepingAndSwapping();
ProtoLog.v(WM_DEBUG_STATES, "Stopping %s: nowVisible=%b animating=%b "
+ "finishing=%s", s, s.nowVisible, animating, s.finishing);
- if ((!animating && !displaySwapping) || mService.mShuttingDown) {
+ if ((!animating && !displaySwapping) || mService.mShuttingDown
+ || s.getRootTask().isForceHiddenForPinnedTask()) {
if (!processPausingActivities && s.isState(PAUSING)) {
// Defer processing pausing activities in this iteration and reschedule
// a delayed idle to reprocess it again
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index 89f044bdd163..d7667d8ce7a8 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -215,8 +215,7 @@ class Dimmer {
return mDimState;
}
- private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer,
- float alpha, int blurRadius) {
+ private void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) {
final DimState d = getDimState(container);
if (d == null) {
@@ -226,6 +225,7 @@ class Dimmer {
// The dim method is called from WindowState.prepareSurfaces(), which is always called
// in the correct Z from lowest Z to highest. This ensures that the dim layer is always
// relative to the highest Z layer with a dim.
+ SurfaceControl.Transaction t = mHost.getPendingTransaction();
t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
t.setAlpha(d.mDimLayer, alpha);
t.setBackgroundBlurRadius(d.mDimLayer, blurRadius);
@@ -238,26 +238,23 @@ class Dimmer {
* for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset
* and the child should call dimAbove again to request the Dim to continue.
*
- * @param t A transaction in which to apply the Dim.
* @param container The container which to dim above. Should be a child of our host.
* @param alpha The alpha at which to Dim.
*/
- void dimAbove(SurfaceControl.Transaction t, WindowContainer container, float alpha) {
- dim(t, container, 1, alpha, 0);
+ void dimAbove(WindowContainer container, float alpha) {
+ dim(container, 1, alpha, 0);
}
/**
* Like {@link #dimAbove} but places the dim below the given container.
*
- * @param t A transaction in which to apply the Dim.
* @param container The container which to dim below. Should be a child of our host.
* @param alpha The alpha at which to Dim.
* @param blurRadius The amount of blur added to the Dim.
*/
- void dimBelow(SurfaceControl.Transaction t, WindowContainer container, float alpha,
- int blurRadius) {
- dim(t, container, -1, alpha, blurRadius);
+ void dimBelow(WindowContainer container, float alpha, int blurRadius) {
+ dim(container, -1, alpha, blurRadius);
}
/**
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index cb7414e2e86e..64c2c5d9c228 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1935,7 +1935,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
} else if (mFixedRotationLaunchingApp != null && r == null) {
mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this);
// Keep async rotation controller if the next transition of display is requested.
- if (!mTransitionController.isCollecting(this)) {
+ if (!mTransitionController.hasCollectingRotationChange(this, getRotation())) {
finishAsyncRotationIfPossible();
}
}
@@ -2751,6 +2751,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
void onAppTransitionDone() {
super.onAppTransitionDone();
mWmService.mWindowsChanged = true;
+ onTransitionFinished();
+ }
+
+ void onTransitionFinished() {
// If the transition finished callback cannot match the token for some reason, make sure the
// rotated state is cleared if it is already invisible.
if (mFixedRotationLaunchingApp != null && !mFixedRotationLaunchingApp.isVisibleRequested()
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 2717a6a8ab04..354b0db77382 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -22,6 +22,7 @@ import static android.view.InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE;
import static android.view.InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS;
import static android.view.InsetsFrameProvider.SOURCE_DISPLAY;
import static android.view.InsetsFrameProvider.SOURCE_FRAME;
+import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -38,6 +39,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACK
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
@@ -194,6 +196,8 @@ public class DisplayPolicy {
private final ScreenshotHelper mScreenshotHelper;
private final Object mServiceAcquireLock = new Object();
+ private long mPanicTime;
+ private final long mPanicThresholdMs;
private StatusBarManagerInternal mStatusBarManagerInternal;
@Px
@@ -246,6 +250,8 @@ public class DisplayPolicy {
private volatile boolean mKeyguardDrawComplete;
private volatile boolean mWindowManagerDrawComplete;
+ private boolean mImmersiveConfirmationWindowExists;
+
private WindowState mStatusBar = null;
private volatile WindowState mNotificationShade;
private WindowState mNavigationBar = null;
@@ -402,6 +408,7 @@ public class DisplayPolicy {
mCanSystemBarsBeShownByUser = !r.getBoolean(
R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
+ mPanicThresholdMs = r.getInteger(R.integer.config_immersive_mode_confirmation_panic);
mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(
Context.ACCESSIBILITY_SERVICE);
@@ -623,8 +630,12 @@ public class DisplayPolicy {
};
displayContent.mAppTransition.registerListenerLocked(mAppTransitionListener);
displayContent.mTransitionController.registerLegacyListener(mAppTransitionListener);
- mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
- mService.mVrModeEnabled, mCanSystemBarsBeShownByUser);
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation = null;
+ } else {
+ mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper,
+ mService.mVrModeEnabled, mCanSystemBarsBeShownByUser);
+ }
// TODO: Make it can take screenshot on external display
mScreenshotHelper = displayContent.isDefaultDisplay
@@ -1075,6 +1086,9 @@ public class DisplayPolicy {
mNavigationBar = win;
break;
}
+ if ((attrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
+ mImmersiveConfirmationWindowExists = true;
+ }
if (attrs.providedInsets != null) {
for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
final InsetsFrameProvider provider = attrs.providedInsets[i];
@@ -1234,6 +1248,9 @@ public class DisplayPolicy {
}
}
mInsetsSourceWindowsExceptIme.remove(win);
+ if ((win.mAttrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
+ mImmersiveConfirmationWindowExists = false;
+ }
}
WindowState getStatusBar() {
@@ -2171,7 +2188,11 @@ public class DisplayPolicy {
}
}
}
- mImmersiveModeConfirmation.confirmCurrentPrompt();
+ if (CLIENT_IMMERSIVE_CONFIRMATION || CLIENT_TRANSIENT) {
+ mStatusBarManagerInternal.confirmImmersivePrompt();
+ } else {
+ mImmersiveModeConfirmation.confirmCurrentPrompt();
+ }
}
boolean isKeyguardShowing() {
@@ -2221,7 +2242,8 @@ public class DisplayPolicy {
// Immersive mode confirmation should never affect the system bar visibility, otherwise
// it will unhide the navigation bar and hide itself.
- if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) {
+ if ((winCandidate.getAttrs().privateFlags
+ & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) {
if (mNotificationShade != null && mNotificationShade.canReceiveKeys()) {
// Let notification shade control the system bar visibility.
winCandidate = mNotificationShade;
@@ -2389,9 +2411,16 @@ public class DisplayPolicy {
// The immersive confirmation window should be attached to the immersive window root.
final RootDisplayArea root = win.getRootDisplayArea();
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
- mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, isImmersiveMode,
- mService.mPolicy.isUserSetupComplete(),
- isNavBarEmpty(disableFlags));
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId,
+ isImmersiveMode,
+ mService.mPolicy.isUserSetupComplete(),
+ isNavBarEmpty(disableFlags));
+ } else {
+ // TODO (b/277290737): Move this to the client side, instead of using a proxy.
+ callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(rootDisplayAreaId,
+ isImmersiveMode));
+ }
}
// Show transient bars for panic if needed.
@@ -2604,16 +2633,39 @@ public class DisplayPolicy {
void onPowerKeyDown(boolean isScreenOn) {
// Detect user pressing the power button in panic when an application has
// taken over the whole screen.
- boolean panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn,
- SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow),
- isNavBarEmpty(mLastDisableFlags));
+ boolean panic = false;
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn,
+ SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow),
+ isNavBarEmpty(mLastDisableFlags));
+ } else {
+ panic = isPowerKeyDownPanic(isScreenOn, SystemClock.elapsedRealtime(),
+ isImmersiveMode(mSystemUiControllingWindow), isNavBarEmpty(mLastDisableFlags));
+ }
if (panic) {
mHandler.post(mHiddenNavPanic);
}
}
+ private boolean isPowerKeyDownPanic(boolean isScreenOn, long time, boolean inImmersiveMode,
+ boolean navBarEmpty) {
+ if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) {
+ // turning the screen back on within the panic threshold
+ return !mImmersiveConfirmationWindowExists;
+ }
+ if (isScreenOn && inImmersiveMode && !navBarEmpty) {
+ // turning the screen off, remember if we were in immersive mode
+ mPanicTime = time;
+ } else {
+ mPanicTime = 0;
+ }
+ return false;
+ }
+
void onVrStateChangedLw(boolean enabled) {
- mImmersiveModeConfirmation.onVrStateChangedLw(enabled);
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.onVrStateChangedLw(enabled);
+ }
}
/**
@@ -2626,7 +2678,9 @@ public class DisplayPolicy {
* {@link ActivityManager#LOCK_TASK_MODE_PINNED}.
*/
public void onLockTaskStateChangedLw(int lockTaskState) {
- mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
+ }
}
/** Called when a {@link android.os.PowerManager#USER_ACTIVITY_EVENT_TOUCH} is sent. */
@@ -2643,7 +2697,11 @@ public class DisplayPolicy {
}
boolean onSystemUiSettingsChanged() {
- return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
+ if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
+ return false;
+ } else {
+ return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
+ }
}
/**
@@ -2857,7 +2915,9 @@ public class DisplayPolicy {
mDisplayContent.mTransitionController.unregisterLegacyListener(mAppTransitionListener);
mHandler.post(mGestureNavigationSettingsObserver::unregister);
mHandler.post(mForceShowNavBarSettingsObserver::unregister);
- mImmersiveModeConfirmation.release();
+ if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
+ mImmersiveModeConfirmation.release();
+ }
if (mService.mPointerLocationEnabled) {
setPointerLocationEnabled(false);
}
diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
index 56edde09f747..bd08dff9481b 100644
--- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
+++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
@@ -229,7 +230,8 @@ public class ImmersiveModeConfirmation {
lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
// Trusted overlay so touches outside the touchable area are allowed to pass through
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
- | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+ | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
lp.setTitle("ImmersiveModeConfirmation");
lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
lp.token = getWindowToken();
@@ -469,6 +471,9 @@ public class ImmersiveModeConfirmation {
@Override
public void handleMessage(Message msg) {
+ if (CLIENT_TRANSIENT) {
+ return;
+ }
switch(msg.what) {
case SHOW:
handleShow(msg.arg1);
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 2b8312c3ea60..5f3d517b2cdf 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -81,6 +81,7 @@ class InsetsSourceProvider {
private boolean mIsLeashReadyForDispatching;
private final Rect mSourceFrame = new Rect();
private final Rect mLastSourceFrame = new Rect();
+ private final Rect mLastContainerBounds = new Rect();
private @NonNull Insets mInsetsHint = Insets.NONE;
private @Flags int mFlagsFromFrameProvider;
private @Flags int mFlagsFromServer;
@@ -278,11 +279,31 @@ class InsetsSourceProvider {
// visible. (i.e. No surface, pending insets that were given during layout, etc..)
if (mServerVisible) {
mSource.setFrame(mSourceFrame);
+ updateInsetsHint();
} else {
mSource.setFrame(0, 0, 0, 0);
}
}
+ // To be called when mSourceFrame or the window container bounds is changed.
+ private void updateInsetsHint() {
+ if (!mControllable || !mServerVisible) {
+ return;
+ }
+ final Rect bounds = mWindowContainer.getBounds();
+ if (mSourceFrame.equals(mLastSourceFrame) && bounds.equals(mLastContainerBounds)) {
+ return;
+ }
+ mLastSourceFrame.set(mSourceFrame);
+ mLastContainerBounds.set(bounds);
+ mInsetsHint = mSource.calculateInsets(bounds, true /* ignoreVisibility */);
+ }
+
+ @VisibleForTesting
+ Insets getInsetsHint() {
+ return mInsetsHint;
+ }
+
/** @return A new source computed by the specified window frame in the given display frames. */
InsetsSource createSimulatedSource(DisplayFrames displayFrames, Rect frame) {
final InsetsSource source = new InsetsSource(mSource);
@@ -338,15 +359,9 @@ class InsetsSourceProvider {
mSetLeashPositionConsumer.accept(t);
}
}
- if (mServerVisible && !mLastSourceFrame.equals(mSource.getFrame())) {
- final Insets insetsHint = mSource.calculateInsets(
- mWindowContainer.getBounds(), true /* ignoreVisibility */);
- if (!insetsHint.equals(mControl.getInsetsHint())) {
- changed = true;
- mControl.setInsetsHint(insetsHint);
- mInsetsHint = insetsHint;
- }
- mLastSourceFrame.set(mSource.getFrame());
+ if (!mControl.getInsetsHint().equals(mInsetsHint)) {
+ mControl.setInsetsHint(mInsetsHint);
+ changed = true;
}
if (changed) {
mStateController.notifyControlChanged(mControlTarget);
@@ -587,6 +602,11 @@ class InsetsSourceProvider {
pw.print(prefix + "mControl=");
mControl.dump("", pw);
}
+ if (mControllable) {
+ pw.print(prefix + "mInsetsHint=");
+ pw.print(mInsetsHint);
+ pw.println();
+ }
pw.print(prefix);
pw.print("mIsLeashReadyForDispatching="); pw.print(mIsLeashReadyForDispatching);
pw.println();
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 7a201a77c966..9e3a611c0e70 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -85,6 +85,11 @@ final class LetterboxConfiguration {
// TODO(b/288142656): Enable user aspect ratio settings by default.
private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = false;
+ // Whether per-app fullscreen user aspect ratio override option is enabled
+ private static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
+ "enable_app_compat_user_aspect_ratio_fullscreen";
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
+
// Whether the letterbox wallpaper style is enabled by default
private static final String KEY_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER =
"enable_letterbox_background_wallpaper";
@@ -266,6 +271,9 @@ final class LetterboxConfiguration {
// Allows to enable user aspect ratio settings ignoring flags.
private boolean mUserAppAspectRatioSettingsOverrideEnabled;
+ // Allows to enable fullscreen option in user aspect ratio settings ignoring flags.
+ private boolean mUserAppAspectRatioFullscreenOverrideEnabled;
+
// The override for letterbox background type in case it's different from
// LETTERBOX_BACKGROUND_OVERRIDE_UNSET
@LetterboxBackgroundType
@@ -294,16 +302,15 @@ final class LetterboxConfiguration {
@NonNull private final SynchedDeviceConfig mDeviceConfig;
LetterboxConfiguration(@NonNull final Context systemUiContext) {
- this(systemUiContext,
- new LetterboxConfigurationPersister(systemUiContext,
- () -> readLetterboxHorizontalReachabilityPositionFromConfig(
- systemUiContext, /* forBookMode */ false),
- () -> readLetterboxVerticalReachabilityPositionFromConfig(
- systemUiContext, /* forTabletopMode */ false),
- () -> readLetterboxHorizontalReachabilityPositionFromConfig(
- systemUiContext, /* forBookMode */ true),
- () -> readLetterboxVerticalReachabilityPositionFromConfig(
- systemUiContext, /* forTabletopMode */ true)));
+ this(systemUiContext, new LetterboxConfigurationPersister(
+ () -> readLetterboxHorizontalReachabilityPositionFromConfig(
+ systemUiContext, /* forBookMode */ false),
+ () -> readLetterboxVerticalReachabilityPositionFromConfig(
+ systemUiContext, /* forTabletopMode */ false),
+ () -> readLetterboxHorizontalReachabilityPositionFromConfig(
+ systemUiContext, /* forBookMode */ true),
+ () -> readLetterboxVerticalReachabilityPositionFromConfig(
+ systemUiContext, /* forTabletopMode */ true)));
}
@VisibleForTesting
@@ -380,6 +387,10 @@ final class LetterboxConfiguration {
R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled))
.addDeviceConfigEntry(KEY_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER,
DEFAULT_VALUE_ENABLE_LETTERBOX_BACKGROUND_WALLPAPER, /* enabled */ true)
+ .addDeviceConfigEntry(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ mContext.getResources().getBoolean(
+ R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled))
.build();
}
@@ -1276,4 +1287,21 @@ final class LetterboxConfiguration {
void resetUserAppAspectRatioSettingsEnabled() {
setUserAppAspectRatioSettingsOverrideEnabled(false);
}
+
+ /**
+ * Whether fullscreen option in per-app user aspect ratio settings is enabled
+ */
+ boolean isUserAppAspectRatioFullscreenEnabled() {
+ return isUserAppAspectRatioSettingsEnabled()
+ && (mUserAppAspectRatioFullscreenOverrideEnabled
+ || mDeviceConfig.getFlagValue(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN));
+ }
+
+ void setUserAppAspectRatioFullscreenOverrideEnabled(boolean enabled) {
+ mUserAppAspectRatioFullscreenOverrideEnabled = enabled;
+ }
+
+ void resetUserAppAspectRatioFullscreenEnabled() {
+ setUserAppAspectRatioFullscreenOverrideEnabled(false);
+ }
}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
index 756339701590..38aa903e3954 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -23,7 +23,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.Context;
import android.os.Environment;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
@@ -53,10 +52,8 @@ class LetterboxConfigurationPersister {
private static final String TAG =
TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM;
- @VisibleForTesting
- static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
+ private static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
- private final Context mContext;
private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
private final Supplier<Integer> mDefaultBookModeReachabilitySupplier;
@@ -97,36 +94,32 @@ class LetterboxConfigurationPersister {
@NonNull
private final PersisterQueue mPersisterQueue;
- LetterboxConfigurationPersister(Context systemUiContext,
- Supplier<Integer> defaultHorizontalReachabilitySupplier,
- Supplier<Integer> defaultVerticalReachabilitySupplier,
- Supplier<Integer> defaultBookModeReachabilitySupplier,
- Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
- this(systemUiContext, defaultHorizontalReachabilitySupplier,
- defaultVerticalReachabilitySupplier,
- defaultBookModeReachabilitySupplier,
- defaultTabletopModeReachabilitySupplier,
+ LetterboxConfigurationPersister(
+ @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
+ @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
+ @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
+ @NonNull Supplier<Integer> defaultTabletopModeReachabilitySupplier) {
+ this(defaultHorizontalReachabilitySupplier, defaultVerticalReachabilitySupplier,
+ defaultBookModeReachabilitySupplier, defaultTabletopModeReachabilitySupplier,
Environment.getDataSystemDirectory(), new PersisterQueue(),
- /* completionCallback */ null);
+ /* completionCallback */ null, LETTERBOX_CONFIGURATION_FILENAME);
}
@VisibleForTesting
- LetterboxConfigurationPersister(Context systemUiContext,
- Supplier<Integer> defaultHorizontalReachabilitySupplier,
- Supplier<Integer> defaultVerticalReachabilitySupplier,
- Supplier<Integer> defaultBookModeReachabilitySupplier,
- Supplier<Integer> defaultTabletopModeReachabilitySupplier,
- File configFolder,
- PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
- mContext = systemUiContext.createDeviceProtectedStorageContext();
+ LetterboxConfigurationPersister(
+ @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier,
+ @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier,
+ @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier,
+ @NonNull Supplier<Integer> defaultTabletopModeReachabilitySupplier,
+ @NonNull File configFolder, @NonNull PersisterQueue persisterQueue,
+ @Nullable Consumer<String> completionCallback,
+ @NonNull String letterboxConfigurationFileName) {
mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
- mDefaultBookModeReachabilitySupplier =
- defaultBookModeReachabilitySupplier;
- mDefaultTabletopModeReachabilitySupplier =
- defaultTabletopModeReachabilitySupplier;
+ mDefaultBookModeReachabilitySupplier = defaultBookModeReachabilitySupplier;
+ mDefaultTabletopModeReachabilitySupplier = defaultTabletopModeReachabilitySupplier;
mCompletionCallback = completionCallback;
- final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
+ final File prefFiles = new File(configFolder, letterboxConfigurationFileName);
mConfigurationFile = new AtomicFile(prefFiles);
mPersisterQueue = persisterQueue;
runWithDiskReadsThreadPolicy(this::readCurrentConfiguration);
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 39f75703d71f..394105a646f1 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -40,6 +40,12 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.content.pm.ActivityInfo.isFixedOrientation;
import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
import static android.content.pm.ActivityInfo.screenOrientationToString;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
@@ -103,6 +109,7 @@ import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
+import android.os.RemoteException;
import android.util.Slog;
import android.view.InsetsSource;
import android.view.InsetsState;
@@ -237,6 +244,10 @@ final class LetterboxUiController {
// Counter for ActivityRecord#setRequestedOrientation
private int mSetOrientationRequestCounter = 0;
+ // The min aspect ratio override set by user
+ @PackageManager.UserMinAspectRatio
+ private int mUserAspectRatio = USER_MIN_ASPECT_RATIO_UNSET;
+
// The CompatDisplayInsets of the opaque activity beneath the translucent one.
private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets;
@@ -1059,7 +1070,7 @@ final class LetterboxUiController {
private float getDefaultMinAspectRatioForUnresizableApps() {
if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
- || mActivityRecord.getDisplayContent() == null) {
+ || mActivityRecord.getDisplayArea() == null) {
return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
> MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
@@ -1071,8 +1082,8 @@ final class LetterboxUiController {
float getSplitScreenAspectRatio() {
// Getting the same aspect ratio that apps get in split screen.
- final DisplayContent displayContent = mActivityRecord.getDisplayContent();
- if (displayContent == null) {
+ final DisplayArea displayArea = mActivityRecord.getDisplayArea();
+ if (displayArea == null) {
return getDefaultMinAspectRatioForUnresizableApps();
}
int dividerWindowWidth =
@@ -1080,7 +1091,7 @@ final class LetterboxUiController {
int dividerInsets =
getResources().getDimensionPixelSize(R.dimen.docked_stack_divider_insets);
int dividerSize = dividerWindowWidth - dividerInsets * 2;
- final Rect bounds = new Rect(displayContent.getWindowConfiguration().getAppBounds());
+ final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
if (bounds.width() >= bounds.height()) {
bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
bounds.right = bounds.centerX();
@@ -1091,14 +1102,57 @@ final class LetterboxUiController {
return computeAspectRatio(bounds);
}
+ boolean shouldApplyUserMinAspectRatioOverride() {
+ if (!mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled()) {
+ return false;
+ }
+
+ try {
+ final int userAspectRatio = mActivityRecord.mAtmService.getPackageManager()
+ .getUserMinAspectRatio(mActivityRecord.packageName, mActivityRecord.mUserId);
+ mUserAspectRatio = userAspectRatio;
+ return userAspectRatio != USER_MIN_ASPECT_RATIO_UNSET;
+ } catch (RemoteException e) {
+ // Don't apply user aspect ratio override
+ Slog.w(TAG, "Exception thrown retrieving aspect ratio user override " + this, e);
+ return false;
+ }
+ }
+
+ float getUserMinAspectRatio() {
+ switch (mUserAspectRatio) {
+ case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ return getDisplaySizeMinAspectRatio();
+ case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ return getSplitScreenAspectRatio();
+ case USER_MIN_ASPECT_RATIO_16_9:
+ return 16 / 9f;
+ case USER_MIN_ASPECT_RATIO_4_3:
+ return 4 / 3f;
+ case USER_MIN_ASPECT_RATIO_3_2:
+ return 3 / 2f;
+ default:
+ throw new AssertionError("Unexpected user min aspect ratio override: "
+ + mUserAspectRatio);
+ }
+ }
+
+ private float getDisplaySizeMinAspectRatio() {
+ final DisplayArea displayArea = mActivityRecord.getDisplayArea();
+ if (displayArea == null) {
+ return mActivityRecord.info.getMinAspectRatio();
+ }
+ final Rect bounds = new Rect(displayArea.getWindowConfiguration().getAppBounds());
+ return computeAspectRatio(bounds);
+ }
+
private float getDefaultMinAspectRatio() {
- final DisplayContent displayContent = mActivityRecord.getDisplayContent();
- if (displayContent == null
+ if (mActivityRecord.getDisplayArea() == null
|| !mLetterboxConfiguration
.getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
return mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
}
- return computeAspectRatio(new Rect(displayContent.getBounds()));
+ return getDisplaySizeMinAspectRatio();
}
Resources getResources() {
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 9ef5ed051a13..4faaf5170f27 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -682,6 +682,26 @@ class RecentTasks {
}
}
+ /**
+ * Removes the oldest recent task that is compatible with the given one. This is possible if
+ * the task windowing mode changed after being added to the Recents.
+ */
+ void removeCompatibleRecentTask(Task task) {
+ final int taskIndex = mTasks.indexOf(task);
+ if (taskIndex < 0) {
+ return;
+ }
+
+ final int candidateIndex = findRemoveIndexForTask(task, false /* includingSelf */);
+ if (candidateIndex == -1) {
+ // Nothing to trim
+ return;
+ }
+
+ final Task taskToRemove = taskIndex > candidateIndex ? task : mTasks.get(candidateIndex);
+ remove(taskToRemove);
+ }
+
void removeTasksByPackageName(String packageName, int userId) {
for (int i = mTasks.size() - 1; i >= 0; --i) {
final Task task = mTasks.get(i);
@@ -1540,6 +1560,10 @@ class RecentTasks {
* list (if any).
*/
private int findRemoveIndexForAddTask(Task task) {
+ return findRemoveIndexForTask(task, true /* includingSelf */);
+ }
+
+ private int findRemoveIndexForTask(Task task, boolean includingSelf) {
final int recentsCount = mTasks.size();
final Intent intent = task.intent;
final boolean document = intent != null && intent.isDocument();
@@ -1595,6 +1619,8 @@ class RecentTasks {
// existing task
continue;
}
+ } else if (!includingSelf) {
+ continue;
}
return i;
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index d9a954f1973b..05f95f813e55 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -3212,6 +3212,10 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
+ "not idle", rootTask.getRootTaskId(), resumedActivity);
return false;
}
+ if (mTransitionController.isTransientLaunch(resumedActivity)) {
+ // Not idle if the transient transition animation is running.
+ return false;
+ }
}
// End power mode launch when idle.
mService.endLaunchPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9c23beb21a92..6caccddcab2b 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4504,6 +4504,10 @@ class Task extends TaskFragment {
return mForceHiddenFlags != 0;
}
+ boolean isForceHiddenForPinnedTask() {
+ return (mForceHiddenFlags & FLAG_FORCE_HIDDEN_FOR_PINNED_TASK) != 0;
+ }
+
@Override
protected boolean isForceTranslucent() {
return mForceTranslucent;
@@ -5251,17 +5255,21 @@ class Task extends TaskFragment {
// Ensure that we do not trigger entering PiP an activity on the root pinned task.
return;
}
- final boolean isTransient = opts != null && opts.getTransientLaunch();
- final Task targetRootTask = toFrontTask != null
- ? toFrontTask.getRootTask() : toFrontActivity.getRootTask();
- if (targetRootTask != null && (targetRootTask.isActivityTypeAssistant() || isTransient)) {
- // Ensure the task/activity being brought forward is not the assistant and is not
- // transient. In the case of transient-launch, we want to wait until the end of the
- // transition and only allow switch if the transient launch was committed.
+ final Task targetRootTask = toFrontTask != null ? toFrontTask.getRootTask()
+ : toFrontActivity != null ? toFrontActivity.getRootTask() : null;
+ if (targetRootTask == null) {
+ Slog.e(TAG, "No root task for enter pip, both to front task and activity are null?");
return;
}
- pipCandidate.supportsEnterPipOnTaskSwitch = true;
+ final boolean isTransient = opts != null && opts.getTransientLaunch()
+ || (targetRootTask.mTransitionController.isTransientHide(targetRootTask));
+ // Ensure the task/activity being brought forward is not the assistant and is not transient
+ // nor transient hide target. In the case of transient-launch, we want to wait until the end
+ // of the transition and only allow to enter pip on task switch after the transient launch
+ // was committed.
+ pipCandidate.supportsEnterPipOnTaskSwitch = !targetRootTask.isActivityTypeAssistant()
+ && !isTransient;
}
/**
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 0c1f33ccedbc..57ce368aae87 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -329,6 +329,15 @@ class TaskFragment extends WindowContainer<WindowContainer> {
*/
private boolean mDelayLastActivityRemoval;
+ /**
+ * Whether the activity navigation should be isolated. That is, Activities cannot be launched
+ * on an isolated TaskFragment, unless the activity is launched from an Activity in the same
+ * isolated TaskFragment, or explicitly requested to be launched to.
+ * <p>
+ * Note that only an embedded TaskFragment can be isolated.
+ */
+ private boolean mIsolatedNav;
+
final Point mLastSurfaceSize = new Point();
private final Rect mTmpBounds = new Rect();
@@ -481,6 +490,19 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return mAnimationParams;
}
+ /** @see #mIsolatedNav */
+ void setIsolatedNav(boolean isolatedNav) {
+ if (!isEmbedded()) {
+ return;
+ }
+ mIsolatedNav = isolatedNav;
+ }
+
+ /** @see #mIsolatedNav */
+ boolean isIsolatedNav() {
+ return isEmbedded() && mIsolatedNav;
+ }
+
TaskFragment getAdjacentTaskFragment() {
return mAdjacentTaskFragment;
}
@@ -1020,7 +1042,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
final WindowContainer<?> parent = getParent();
final Task thisTask = asTask();
if (thisTask != null && parent.asTask() == null
- && mTransitionController.isTransientHide(thisTask)) {
+ && mTransitionController.isTransientVisible(thisTask)) {
// Keep transient-hide root tasks visible. Non-root tasks still follow standard rule.
return TASK_FRAGMENT_VISIBILITY_VISIBLE;
}
@@ -3034,7 +3056,8 @@ class TaskFragment extends WindowContainer<WindowContainer> {
@Override
void dump(PrintWriter pw, String prefix, boolean dumpAll) {
super.dump(pw, prefix, dumpAll);
- pw.println(prefix + "bounds=" + getBounds().toShortString());
+ pw.println(prefix + "bounds=" + getBounds().toShortString()
+ + (mIsolatedNav ? ", isolatedNav" : ""));
final String doublePrefix = prefix + " ";
for (int i = mChildren.size() - 1; i >= 0; i--) {
final WindowContainer<?> child = mChildren.get(i);
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 71192cd5a3be..b7c8092e3774 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -407,6 +407,36 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
return false;
}
+ /** Returns {@code true} if the task should keep visible if this is a transient transition. */
+ boolean isTransientVisible(@NonNull Task task) {
+ if (mTransientLaunches == null) return false;
+ int occludedCount = 0;
+ final int numTransient = mTransientLaunches.size();
+ for (int i = numTransient - 1; i >= 0; --i) {
+ final Task transientRoot = mTransientLaunches.keyAt(i).getRootTask();
+ if (transientRoot == null) continue;
+ final WindowContainer<?> rootParent = transientRoot.getParent();
+ if (rootParent == null || rootParent.getTopChild() == transientRoot) continue;
+ final ActivityRecord topOpaque = mController.mAtm.mTaskSupervisor
+ .mOpaqueActivityHelper.getOpaqueActivity(rootParent);
+ if (transientRoot.compareTo(topOpaque.getRootTask()) < 0) {
+ occludedCount++;
+ }
+ }
+ if (occludedCount == numTransient) {
+ for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
+ if (mTransientLaunches.keyAt(i).isDescendantOf(task)) {
+ // Keep transient activity visible until transition finished, so it won't pause
+ // with transient-hide tasks that may delay resuming the next top.
+ return true;
+ }
+ }
+ // Let transient-hide activities pause before transition is finished.
+ return false;
+ }
+ return isInTransientHide(task);
+ }
+
boolean canApplyDim(@NonNull Task task) {
if (mTransientLaunches == null) return true;
final Dimmer dimmer = task.getDimmer();
@@ -1298,6 +1328,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
if (asyncRotationController != null && containsChangeFor(dc, mTargets)) {
asyncRotationController.onTransitionFinished();
}
+ dc.onTransitionFinished();
if (hasParticipatedDisplay && dc.mDisplayRotationCompatPolicy != null) {
final ChangeInfo changeInfo = mChanges.get(dc);
if (changeInfo != null
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 79cb61be5948..1565341deb4c 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -30,6 +30,7 @@ import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.IApplicationThread;
import android.app.WindowConfiguration;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.IBinder;
@@ -141,6 +142,14 @@ class TransitionController {
final ArrayList<ActivityRecord> mValidateCommitVis = new ArrayList<>();
/**
+ * List of activity-level participants. ActivityRecord is not expected to change independently,
+ * however, recent compatibility logic can now cause this at arbitrary times determined by
+ * client code. If it happens during an animation, the surface can be left at the wrong spot.
+ * TODO(b/290237710) remove when compat logic is moved.
+ */
+ final ArrayList<ActivityRecord> mValidateActivityCompat = new ArrayList<>();
+
+ /**
* Currently playing transitions (in the order they were started). When finished, records are
* removed from this list.
*/
@@ -468,15 +477,22 @@ class TransitionController {
if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
return true;
}
- for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
- if (mWaitingTransitions.get(i).isInTransientHide(task)) return true;
- }
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isInTransientHide(task)) return true;
}
return false;
}
+ boolean isTransientVisible(@NonNull Task task) {
+ if (mCollectingTransition != null && mCollectingTransition.isTransientVisible(task)) {
+ return true;
+ }
+ for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+ if (mPlayingTransitions.get(i).isTransientVisible(task)) return true;
+ }
+ return false;
+ }
+
boolean canApplyDim(@Nullable Task task) {
if (task == null) {
// Always allow non-activity window.
@@ -562,6 +578,17 @@ class TransitionController {
}
/**
+ * Returns {@code true} if the window container is in the collecting transition, and its
+ * collected rotation is different from the target rotation.
+ */
+ boolean hasCollectingRotationChange(@NonNull WindowContainer<?> wc, int targetRotation) {
+ final Transition transition = mCollectingTransition;
+ if (transition == null || !transition.mParticipants.contains(wc)) return false;
+ final Transition.ChangeInfo changeInfo = transition.mChanges.get(wc);
+ return changeInfo != null && changeInfo.mRotation != targetRotation;
+ }
+
+ /**
* @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition)
*/
@Nullable
@@ -896,6 +923,14 @@ class TransitionController {
}
}
mValidateCommitVis.clear();
+ for (int i = 0; i < mValidateActivityCompat.size(); ++i) {
+ ActivityRecord ar = mValidateActivityCompat.get(i);
+ if (ar.getSurfaceControl() == null) continue;
+ final Point tmpPos = new Point();
+ ar.getRelativePosition(tmpPos);
+ ar.getSyncTransaction().setPosition(ar.getSurfaceControl(), tmpPos.x, tmpPos.y);
+ }
+ mValidateActivityCompat.clear();
}
/**
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0e19671afd82..3a19a3b1e56a 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1996,7 +1996,7 @@ public class WindowManagerService extends IWindowManager.Stub
}
if (win.mActivityRecord != null) {
- win.mActivityRecord.postWindowRemoveStartingWindowCleanup(win);
+ win.mActivityRecord.postWindowRemoveStartingWindowCleanup();
}
if (win.mAttrs.type == TYPE_WALLPAPER) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index f4781f9bc9f0..ceebb27642ce 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1013,6 +1013,10 @@ public class WindowManagerShellCommand extends ShellCommand {
runSetBooleanFlag(pw, mLetterboxConfiguration
::setUserAppAspectRatioSettingsOverrideEnabled);
break;
+ case "--isUserAppAspectRatioFullscreenEnabled":
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setUserAppAspectRatioFullscreenOverrideEnabled);
+ break;
case "--isCameraCompatRefreshEnabled":
runSetBooleanFlag(pw, mLetterboxConfiguration::setCameraCompatRefreshEnabled);
break;
@@ -1093,6 +1097,9 @@ public class WindowManagerShellCommand extends ShellCommand {
case "isUserAppAspectRatioSettingsEnabled":
mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
break;
+ case "isUserAppAspectRatioFullscreenEnabled":
+ mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
+ break;
case "isCameraCompatRefreshEnabled":
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
break;
@@ -1204,6 +1211,7 @@ public class WindowManagerShellCommand extends ShellCommand {
mLetterboxConfiguration.resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
mLetterboxConfiguration.resetUserAppAspectRatioSettingsEnabled();
+ mLetterboxConfiguration.resetUserAppAspectRatioFullscreenEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
}
@@ -1272,6 +1280,8 @@ public class WindowManagerShellCommand extends ShellCommand {
+ mLetterboxConfiguration.isTranslucentLetterboxingEnabled());
pw.println("Is the user aspect ratio settings enabled: "
+ mLetterboxConfiguration.isUserAppAspectRatioSettingsEnabled());
+ pw.println("Is the fullscreen option in user aspect ratio settings enabled: "
+ + mLetterboxConfiguration.isUserAppAspectRatioFullscreenEnabled());
}
return 0;
}
@@ -1471,6 +1481,8 @@ public class WindowManagerShellCommand extends ShellCommand {
pw.println(" Whether letterboxing for translucent activities is enabled.");
pw.println(" --isUserAppAspectRatioSettingsEnabled [true|1|false|0]");
pw.println(" Whether user aspect ratio settings are enabled.");
+ pw.println(" --isUserAppAspectRatioFullscreenEnabled [true|1|false|0]");
+ pw.println(" Whether user aspect ratio fullscreen option is enabled.");
pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]");
pw.println(" Whether camera compatibility refresh is enabled.");
pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 541fa9413f45..be0f6db24923 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -30,6 +30,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS;
import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN;
@@ -1356,6 +1357,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
break;
}
+ case OP_TYPE_SET_ISOLATED_NAVIGATION: {
+ final boolean isolatedNav = operation.isIsolatedNav();
+ taskFragment.setIsolatedNav(isolatedNav);
+ break;
+ }
}
return effects;
}
@@ -1615,6 +1621,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
final int count = tasksToReparent.size();
for (int i = 0; i < count; ++i) {
final Task task = tasksToReparent.get(i);
+ final int prevWindowingMode = task.getWindowingMode();
if (syncId >= 0) {
addToSyncSet(syncId, task);
}
@@ -1628,6 +1635,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM,
false /*moveParents*/, "processChildrenTaskReparentHierarchyOp");
}
+ // Trim the compatible Recent task (if any) after the Task is reparented and now has
+ // a different windowing mode, in order to prevent redundant Recent tasks after
+ // reparenting.
+ if (prevWindowingMode != task.getWindowingMode()) {
+ mService.mTaskSupervisor.mRecentTasks.removeCompatibleRecentTask(task);
+ }
}
if (transition != null) transition.collect(newParent);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0d4c2d631b2c..140255b2f016 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5132,7 +5132,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private void applyDims() {
if (((mAttrs.flags & FLAG_DIM_BEHIND) != 0 || shouldDrawBlurBehind())
- && isVisibleNow() && !mHidden && mTransitionController.canApplyDim(getTask())) {
+ && mToken.isVisibleRequested() && isVisibleNow() && !mHidden
+ && mTransitionController.canApplyDim(getTask())) {
// Only show the Dimmer when the following is satisfied:
// 1. The window has the flag FLAG_DIM_BEHIND or blur behind is requested
// 2. The WindowToken is not hidden so dims aren't shown when the window is exiting.
@@ -5142,7 +5143,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mIsDimming = true;
final float dimAmount = (mAttrs.flags & FLAG_DIM_BEHIND) != 0 ? mAttrs.dimAmount : 0;
final int blurRadius = shouldDrawBlurBehind() ? mAttrs.getBlurBehindRadius() : 0;
- getDimmer().dimBelow(getSyncTransaction(), this, dimAmount, blurRadius);
+ getDimmer().dimBelow(this, dimAmount, blurRadius);
}
}
@@ -5702,8 +5703,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// window becomes visible while the sync group is still active.
return true;
}
- if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mWinAnimator.mDrawState == HAS_DRAWN
- && !mRedrawForSyncReported && !mWmService.mResizingWindows.contains(this)) {
+ if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mLastConfigReportedToClient && isDrawn()) {
// Complete the sync state immediately for a drawn window that doesn't need to redraw.
onSyncFinishedDrawing();
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index c065cb5f4ebe..9d391654063c 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -108,6 +108,7 @@ static struct {
jmethodID notifySensorEvent;
jmethodID notifySensorAccuracy;
jmethodID notifyStylusGestureStarted;
+ jmethodID isInputMethodConnectionActive;
jmethodID notifyVibratorState;
jmethodID filterInputEvent;
jmethodID interceptKeyBeforeQueueing;
@@ -322,6 +323,7 @@ public:
TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr);
void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
+ bool isInputMethodConnectionActive() override;
/* --- InputDispatcherPolicyInterface implementation --- */
@@ -1306,6 +1308,14 @@ void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t ev
checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted");
}
+bool NativeInputManager::isInputMethodConnectionActive() {
+ JNIEnv* env = jniEnv();
+ const jboolean result =
+ env->CallBooleanMethod(mServiceObj, gServiceClassInfo.isInputMethodConnectionActive);
+ checkAndClearExceptionFromCallback(env, "isInputMethodConnectionActive");
+ return result;
+}
+
bool NativeInputManager::filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) {
ATRACE_CALL();
JNIEnv* env = jniEnv();
@@ -2743,6 +2753,9 @@ int register_android_server_InputManager(JNIEnv* env) {
GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted",
"(IJ)V");
+ GET_METHOD_ID(gServiceClassInfo.isInputMethodConnectionActive, clazz,
+ "isInputMethodConnectionActive", "()Z");
+
GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V");
GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr",
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index fe913b9807cf..f3c2de6f7af1 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -241,7 +241,6 @@ import static android.provider.Telephony.Carriers.ENFORCE_KEY;
import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
import static android.provider.Telephony.Carriers.INVALID_APN_ID;
import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION;
-
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
@@ -540,6 +539,8 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
@@ -18817,10 +18818,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
});
}
+ ThreadPoolExecutor calculateHasIncompatibleAccountsExecutor = new ThreadPoolExecutor(
+ 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+
@Override
public void calculateHasIncompatibleAccounts() {
+ if (calculateHasIncompatibleAccountsExecutor.getQueue().size() > 1) {
+ return;
+ }
new CalculateHasIncompatibleAccountsTask().executeOnExecutor(
- AsyncTask.THREAD_POOL_EXECUTOR, null);
+ calculateHasIncompatibleAccountsExecutor, null);
}
@Nullable
diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
index 212ec14b4939..bef56cec3385 100644
--- a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
@@ -17,7 +17,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.inputmethodtests">
- <uses-sdk android:targetSdkVersion="31" />
<queries>
<intent>
<action android:name="android.view.InputMethod" />
diff --git a/services/tests/InputMethodSystemServerTests/TEST_MAPPING b/services/tests/InputMethodSystemServerTests/TEST_MAPPING
index 77e32a776d57..cedbfd2b2dde 100644
--- a/services/tests/InputMethodSystemServerTests/TEST_MAPPING
+++ b/services/tests/InputMethodSystemServerTests/TEST_MAPPING
@@ -9,5 +9,16 @@
{"exclude-annotation": "org.junit.Ignore"}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "FrameworksImeTests",
+ "options": [
+ {"include-filter": "com.android.inputmethodservice"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
]
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
index 0104f7142bea..b7de74987eb8 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
@@ -18,8 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.inputmethod.imetests">
- <uses-sdk android:targetSdkVersion="31" />
-
<!-- Permissions required for granting and logging -->
<uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
<uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 898658e759c0..e8acb067f625 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -20,6 +20,8 @@ import static com.android.compatibility.common.util.SystemUtil.eventually;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
import android.app.Instrumentation;
import android.content.Context;
import android.content.res.Configuration;
@@ -45,6 +47,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -56,9 +59,9 @@ public class InputMethodServiceTest {
private static final String EDIT_TEXT_DESC = "Input box";
private static final long TIMEOUT_IN_SECONDS = 3;
private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
- "settings put secure show_ime_with_hard_keyboard 1";
+ "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
- "settings put secure show_ime_with_hard_keyboard 0";
+ "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
private Instrumentation mInstrumentation;
private UiDevice mUiDevice;
@@ -82,29 +85,19 @@ public class InputMethodServiceTest {
mUiDevice.freezeRotation();
mUiDevice.setOrientationNatural();
// Waits for input binding ready.
- eventually(
- () -> {
- mInputMethodService =
- InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
- assertThat(mInputMethodService).isNotNull();
-
- // The editor won't bring up keyboard by default.
- assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
- assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
- });
- // Save the original value of show_ime_with_hard_keyboard in Settings.
+ eventually(() -> {
+ mInputMethodService =
+ InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
+ assertThat(mInputMethodService).isNotNull();
+
+ // The editor won't bring up keyboard by default.
+ assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+ assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
+ });
+ // Save the original value of show_ime_with_hard_keyboard from Settings.
mShowImeWithHardKeyboardEnabled = Settings.Secure.getInt(
mInputMethodService.getContentResolver(),
Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0;
- // Disable showing Ime with hard keyboard because it is the precondition the for most test
- // cases
- if (mShowImeWithHardKeyboardEnabled) {
- executeShellCommand(DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
- }
- mInputMethodService.getResources().getConfiguration().keyboard =
- Configuration.KEYBOARD_NOKEYS;
- mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
- Configuration.HARDKEYBOARDHIDDEN_YES;
}
@After
@@ -112,82 +105,141 @@ public class InputMethodServiceTest {
mUiDevice.unfreezeRotation();
executeShellCommand("ime disable " + mInputMethodId);
// Change back the original value of show_ime_with_hard_keyboard in Settings.
- executeShellCommand(mShowImeWithHardKeyboardEnabled ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
+ executeShellCommand(mShowImeWithHardKeyboardEnabled
+ ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
: DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
}
+ /**
+ * This checks that the IME can be shown and hidden by user actions
+ * (i.e. tapping on an EditText, tapping the Home button).
+ */
@Test
- public void testShowHideKeyboard_byUserAction() throws InterruptedException {
+ public void testShowHideKeyboard_byUserAction() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
// Performs click on editor box to bring up the soft keyboard.
Log.i(TAG, "Click on EditText.");
- verifyInputViewStatus(() -> clickOnEditorText(), true /* inputViewStarted */);
+ verifyInputViewStatus(
+ () -> clickOnEditorText(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // Press back key to hide soft keyboard.
- Log.i(TAG, "Press back");
+ // Press home key to hide soft keyboard.
+ Log.i(TAG, "Press home");
verifyInputViewStatus(
- () -> assertThat(mUiDevice.pressHome()).isTrue(), false /* inputViewStarted */);
+ () -> assertThat(mUiDevice.pressHome()).isTrue(),
+ true /* expected */,
+ false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that the IME can be shown and hidden using the WindowInsetsController APIs.
+ */
@Test
- public void testShowHideKeyboard_byApi() throws InterruptedException {
+ public void testShowHideKeyboard_byApi() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
// Triggers to show IME via public API.
verifyInputViewStatus(
() -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
// Triggers to hide IME via public API.
verifyInputViewStatusOnMainSync(
- () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(),
+ true /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
+ */
@Test
- public void testShowHideSelf() throws InterruptedException {
- // IME requests to show itself without any flags: expect shown.
+ public void testShowHideSelf() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // IME request to show itself without any flags, expect shown.
Log.i(TAG, "Call IMS#requestShowSelf(0)");
verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestShowSelf(0), true /* inputViewStarted */);
+ () -> mInputMethodService.requestShowSelf(0 /* flags */),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME requests to hide itself with flag: HIDE_IMPLICIT_ONLY, expect not hide (shown).
+ // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+ false /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME request to hide itself without any flags: expect hidden.
+ // IME request to hide itself without any flags, expect hidden.
Log.i(TAG, "Call IMS#requestHideSelf(0)");
verifyInputViewStatusOnMainSync(
- () -> mInputMethodService.requestHideSelf(0), false /* inputViewStarted */);
+ () -> mInputMethodService.requestHideSelf(0 /* flags */),
+ true /* expected */,
+ false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
- // IME request to show itself with flag SHOW_IMPLICIT: expect shown.
+ // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
- // IME request to hide itself with flag: HIDE_IMPLICIT_ONLY, expect hidden.
+ // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
verifyInputViewStatusOnMainSync(
() -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+ true /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks the return value of IMS#onEvaluateInputViewShown,
+ * when show_ime_with_hard_keyboard is enabled.
+ */
@Test
public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() throws Exception {
- executeShellCommand(ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
- mInstrumentation.waitForIdleSync();
+ setShowImeWithHardKeyboard(true /* enabled */);
- // Simulate connecting a hard keyboard
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
Configuration.HARDKEYBOARDHIDDEN_NO;
+ eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_NO;
+ eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
}
+ /**
+ * This checks the return value of IMSonEvaluateInputViewShown,
+ * when show_ime_with_hard_keyboard is disabled.
+ */
@Test
- public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() {
+ public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
@@ -196,6 +248,8 @@ public class InputMethodServiceTest {
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_NO;
eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
mInputMethodService.getResources().getConfiguration().keyboard =
@@ -205,149 +259,386 @@ public class InputMethodServiceTest {
eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
}
+ /**
+ * This checks that any (implicit or explicit) show request,
+ * when IMS#onEvaluateInputViewShown returns false, results in the IME not being shown.
+ */
@Test
public void testShowSoftInput_disableShowImeWithHardKeyboard() throws Exception {
- // Simulate connecting a hard keyboard
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
Configuration.HARDKEYBOARDHIDDEN_NO;
+
// When InputMethodService#onEvaluateInputViewShown() returns false, the Ime should not be
// shown no matter what the show flag is.
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that an explicit show request results in the IME being shown.
+ */
@Test
public void testShowSoftInputExplicitly() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
// When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the
// Ime should be shown.
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that an implicit show request results in the IME being shown.
+ */
@Test
public void testShowSoftInputImplicitly() throws Exception {
- // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, the
- // Ime should be shown.
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT,
+ // the IME should be shown.
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that an explicit show request when the IME is not previously shown,
+ * and it should be shown in fullscreen mode, results in the IME being shown.
+ */
@Test
- public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
- // When keyboard is off, InputMethodService#onEvaluateInputViewShown returns true, flag is
- // IMPLICIT and InputMethodService#onEvaluateFullScreenMode returns true, the Ime should not
- // be shown.
+ public void testShowSoftInputExplicitly_fullScreenMode() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // Set orientation landscape to enable fullscreen mode.
setOrientation(2);
eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
- // Wait for the TestActivity to be recreated
+ // Wait for the TestActivity to be recreated.
eventually(() ->
assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
- // Get the new TestActivity
+ // Get the new TestActivity.
mActivity = TestActivity.getLastCreatedInstance();
assertThat(mActivity).isNotNull();
InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
- // Wait for the new EditText to be served by InputMethodManager
+ // Wait for the new EditText to be served by InputMethodManager.
+ eventually(() -> assertThat(
+ imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
+
+ /**
+ * This checks that an implicit show request when the IME is not previously shown,
+ * and it should be shown in fullscreen mode, results in the IME not being shown.
+ */
+ @Test
+ public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // Set orientation landscape to enable fullscreen mode.
+ setOrientation(2);
+ eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
+ // Wait for the TestActivity to be recreated.
eventually(() ->
- assertThat(imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+ assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
+ // Get the new TestActivity.
+ mActivity = TestActivity.getLastCreatedInstance();
+ assertThat(mActivity).isNotNull();
+ InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+ // Wait for the new EditText to be served by InputMethodManager.
+ eventually(() -> assertThat(
+ imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that an explicit show request when a hard keyboard is connected,
+ * results in the IME being shown.
+ */
+ @Test
+ public void testShowSoftInputExplicitly_withHardKeyboard() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
+
+ /**
+ * This checks that an implicit show request when a hard keyboard is connected,
+ * results in the IME not being shown.
+ */
@Test
public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
mInputMethodService.getResources().getConfiguration().keyboard =
Configuration.KEYBOARD_QWERTY;
- // When connecting to a hard keyboard and the flag is IMPLICIT, the Ime should not be shown.
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
false /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
+ /**
+ * This checks that an explicit show request followed by connecting a hard keyboard
+ * and a configuration change, still results in the IME being shown.
+ */
@Test
- public void testConfigurationChanged_withKeyboardShownExplicitly() throws InterruptedException {
+ public void testShowSoftInputExplicitly_thenConfigurationChanged() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Start with no hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
verifyInputViewStatusOnMainSync(
() -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
// Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
mInputMethodService.getResources().getConfiguration().orientation =
Configuration.ORIENTATION_LANDSCAPE;
+
verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
mInputMethodService.getResources().getConfiguration()),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
}
+ /**
+ * This checks that an implicit show request followed by connecting a hard keyboard
+ * and a configuration change, does not trigger IMS#onFinishInputView,
+ * but results in the IME being hidden.
+ */
@Test
- public void testConfigurationChanged_withKeyboardShownImplicitly() throws InterruptedException {
+ public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Start with no hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_NOKEYS;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
verifyInputViewStatusOnMainSync(() -> assertThat(
mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ true /* expected */,
true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
// Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
mInputMethodService.getResources().getConfiguration().orientation =
Configuration.ORIENTATION_LANDSCAPE;
- mInputMethodService.getResources().getConfiguration().keyboard =
- Configuration.KEYBOARD_QWERTY;
// Normally, IMS#onFinishInputView will be called when finishing the input view by the user.
// But if IMS#hideWindow is called when receiving a new configuration change, we don't
// expect that it's user-driven to finish the lifecycle of input view with
// IMS#onFinishInputView, because the input view will be re-initialized according to the
- // last mShowSoftRequested state. So in this case we treat the input view is still alive.
+ // last #mShowInputRequested state. So in this case we treat the input view as still alive.
verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
- mInputMethodService.getResources().getConfiguration()),
+ mInputMethodService.getResources().getConfiguration()),
+ true /* expected */,
true /* inputViewStarted */);
assertThat(mInputMethodService.isInputViewShown()).isFalse();
}
- private void verifyInputViewStatus(Runnable runnable, boolean inputViewStarted)
+ /**
+ * This checks that an explicit show request directly followed by an implicit show request,
+ * while a hardware keyboard is connected, still results in the IME being shown
+ * (i.e. the implicit show request is treated as explicit).
+ */
+ @Test
+ public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard()
+ throws Exception {
+ setShowImeWithHardKeyboard(false /* enabled */);
+
+ // Simulate connecting a hard keyboard.
+ mInputMethodService.getResources().getConfiguration().keyboard =
+ Configuration.KEYBOARD_QWERTY;
+ mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+ Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ // Explicit show request.
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Implicit show request.
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+ // This should now consider the implicit show request, but keep the state from the
+ // explicit show request, and thus not hide the keyboard.
+ verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
+ mInputMethodService.getResources().getConfiguration()),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
+
+ /**
+ * This checks that a forced show request directly followed by an explicit show request,
+ * and then a hide not always request, still results in the IME being shown
+ * (i.e. the explicit show request retains the forced state).
+ */
+ @Test
+ public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()
+ throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(),
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ verifyInputViewStatusOnMainSync(() -> assertThat(
+ mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ verifyInputViewStatusOnMainSync(() ->
+ mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS),
+ false /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ }
+
+ /**
+ * This checks that the IME fullscreen mode state is updated after changing orientation.
+ */
+ @Test
+ public void testFullScreenMode() throws Exception {
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ Log.i(TAG, "Set orientation natural");
+ verifyFullscreenMode(() -> setOrientation(0),
+ false /* expected */,
+ true /* orientationPortrait */);
+
+ Log.i(TAG, "Set orientation left");
+ verifyFullscreenMode(() -> setOrientation(1),
+ true /* expected */,
+ false /* orientationPortrait */);
+
+ Log.i(TAG, "Set orientation right");
+ verifyFullscreenMode(() -> setOrientation(2),
+ false /* expected */,
+ false /* orientationPortrait */);
+ }
+
+ private void verifyInputViewStatus(
+ Runnable runnable, boolean expected, boolean inputViewStarted)
throws InterruptedException {
- verifyInputViewStatusInternal(runnable, inputViewStarted, false /*runOnMainSync*/);
+ verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
+ false /* runOnMainSync */);
}
- private void verifyInputViewStatusOnMainSync(Runnable runnable, boolean inputViewStarted)
+ private void verifyInputViewStatusOnMainSync(
+ Runnable runnable, boolean expected, boolean inputViewStarted)
throws InterruptedException {
- verifyInputViewStatusInternal(runnable, inputViewStarted, true /*runOnMainSync*/);
+ verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
+ true /* runOnMainSync */);
}
+ /**
+ * Verifies the status of the Input View after executing the given runnable.
+ *
+ * @param runnable the runnable to execute for showing or hiding the IME.
+ * @param expected whether the runnable is expected to trigger the signal.
+ * @param inputViewStarted the expected state of the Input View after executing the runnable.
+ * @param runOnMainSync whether to execute the runnable on the main thread.
+ */
private void verifyInputViewStatusInternal(
- Runnable runnable, boolean inputViewStarted, boolean runOnMainSync)
+ Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync)
throws InterruptedException {
CountDownLatch signal = new CountDownLatch(1);
mInputMethodService.setCountDownLatchForTesting(signal);
- // Runnable to trigger onStartInputView()/ onFinishInputView()
+ // Runnable to trigger onStartInputView() / onFinishInputView() / onConfigurationChanged()
if (runOnMainSync) {
mInstrumentation.runOnMainSync(runnable);
} else {
runnable.run();
}
- // Waits for onStartInputView() to finish.
mInstrumentation.waitForIdleSync();
- signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ if (expected && !completed) {
+ fail("Timed out waiting for"
+ + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
+ } else if (!expected && completed) {
+ fail("Unexpected call"
+ + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
+ }
// Input is not finished.
assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
}
- @Test
- public void testFullScreenMode() throws Exception {
- Log.i(TAG, "Set orientation natural");
- verifyFullscreenMode(() -> setOrientation(0), true /* orientationPortrait */);
-
- Log.i(TAG, "Set orientation left");
- verifyFullscreenMode(() -> setOrientation(1), false /* orientationPortrait */);
-
- Log.i(TAG, "Set orientation right");
- verifyFullscreenMode(() -> setOrientation(2), false /* orientationPortrait */);
- }
-
private void setOrientation(int orientation) {
// Simple wrapper for catching RemoteException.
try {
@@ -366,7 +657,15 @@ public class InputMethodServiceTest {
}
}
- private void verifyFullscreenMode(Runnable runnable, boolean orientationPortrait)
+ /**
+ * Verifies the IME fullscreen mode state after executing the given runnable.
+ *
+ * @param runnable the runnable to execute for setting the orientation.
+ * @param expected whether the runnable is expected to trigger the signal.
+ * @param orientationPortrait whether the orientation is expected to be portrait.
+ */
+ private void verifyFullscreenMode(
+ Runnable runnable, boolean expected, boolean orientationPortrait)
throws InterruptedException {
CountDownLatch signal = new CountDownLatch(1);
mInputMethodService.setCountDownLatchForTesting(signal);
@@ -379,7 +678,12 @@ public class InputMethodServiceTest {
}
// Waits for onConfigurationChanged() to finish.
mInstrumentation.waitForIdleSync();
- signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ if (expected && !completed) {
+ fail("Timed out waiting for onConfigurationChanged()");
+ } else if (!expected && completed) {
+ fail("Unexpected call onConfigurationChanged()");
+ }
clickOnEditorText();
eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
@@ -416,7 +720,21 @@ public class InputMethodServiceTest {
return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
}
- private String executeShellCommand(String cmd) throws Exception {
+ /**
+ * Sets the value of show_ime_with_hard_keyboard, only if it is different to the default value.
+ *
+ * @param enabled the value to be set.
+ */
+ private void setShowImeWithHardKeyboard(boolean enabled) throws IOException {
+ if (mShowImeWithHardKeyboardEnabled != enabled) {
+ executeShellCommand(enabled
+ ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
+ : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+ mInstrumentation.waitForIdleSync();
+ }
+ }
+
+ private String executeShellCommand(String cmd) throws IOException {
Log.i(TAG, "Run command: " + cmd);
return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
.executeShellCommand(cmd);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 869497c28def..3199e062418f 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -40,7 +40,6 @@ import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.Display;
-import android.view.inputmethod.InputMethodManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -77,9 +76,9 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe
public void testPerformShowIme() throws Exception {
synchronized (ImfLock.class) {
mVisibilityApplier.performShowIme(new Binder() /* showInputToken */,
- null /* statsToken */, InputMethodManager.SHOW_IMPLICIT, null, SHOW_SOFT_INPUT);
+ null /* statsToken */, 0 /* showFlags */, null, SHOW_SOFT_INPUT);
}
- verifyShowSoftInput(false, true, InputMethodManager.SHOW_IMPLICIT);
+ verifyShowSoftInput(false, true, 0 /* showFlags */);
}
@Test
@@ -126,7 +125,7 @@ public class DefaultImeVisibilityApplierTest extends InputMethodManagerServiceTe
@Test
public void testApplyImeVisibility_showImeImplicit() throws Exception {
mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_SHOW_IME_IMPLICIT);
- verifyShowSoftInput(true, true, InputMethodManager.SHOW_IMPLICIT);
+ verifyShowSoftInput(true, true, 0 /* showFlags */);
}
@Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index a38c1626aea1..fae5f86e4007 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -106,7 +106,7 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes
@Test
public void testRequestImeVisibility_showExplicit() {
initImeTargetWindowState(mWindowToken);
- boolean res = mComputer.onImeShowFlags(null, 0 /* show explicit */);
+ boolean res = mComputer.onImeShowFlags(null, 0 /* showFlags */);
mComputer.requestImeVisibility(mWindowToken, res);
final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
@@ -118,6 +118,34 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes
assertThat(mComputer.mRequestedShowExplicitly).isTrue();
}
+ /**
+ * This checks that the state after an explicit show request does not get reset during
+ * a subsequent implicit show request, without an intermediary hide request.
+ */
+ @Test
+ public void testRequestImeVisibility_showExplicit_thenShowImplicit() {
+ initImeTargetWindowState(mWindowToken);
+ mComputer.onImeShowFlags(null, 0 /* showFlags */);
+ assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+
+ mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
+ assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+ }
+
+ /**
+ * This checks that the state after a forced show request does not get reset during
+ * a subsequent explicit show request, without an intermediary hide request.
+ */
+ @Test
+ public void testRequestImeVisibility_showForced_thenShowExplicit() {
+ initImeTargetWindowState(mWindowToken);
+ mComputer.onImeShowFlags(null, InputMethodManager.SHOW_FORCED);
+ assertThat(mComputer.mShowForced).isTrue();
+
+ mComputer.onImeShowFlags(null, 0 /* showFlags */);
+ assertThat(mComputer.mShowForced).isTrue();
+ }
+
@Test
public void testRequestImeVisibility_showImplicit_a11yNoImePolicy() {
// Precondition: set AccessibilityService#SHOW_MODE_HIDDEN policy
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 42d373b9bf3e..e87a34ea17d7 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -19,6 +19,7 @@ package com.android.server.inputmethod;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
@@ -162,7 +163,10 @@ public class InputMethodBindingControllerTest extends InputMethodManagerServiceT
assertThat(mBindingController.getCurToken()).isNotNull();
}
// Wait for onServiceConnected()
- mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ boolean completed = mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+ if (!completed) {
+ fail("Timed out waiting for onServiceConnected()");
+ }
// Verify onServiceConnected() is called and bound successfully.
synchronized (ImfLock.class) {
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
index 8d0e0c4260e8..e1fd2b34d881 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
@@ -43,6 +43,8 @@ android_test_helper_app {
},
export_package_resources: true,
sdk_version: "current",
+
+ certificate: "platform",
}
android_library {
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
index 996322de2c5e..cf7d660a68ef 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
@@ -18,8 +18,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.apps.inputmethod.simpleime">
- <uses-sdk android:targetSdkVersion="31" />
-
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<application android:debuggable="true"
diff --git a/services/tests/displayservicetests/Android.bp b/services/tests/displayservicetests/Android.bp
index f1ff33809184..a421db0bdf46 100644
--- a/services/tests/displayservicetests/Android.bp
+++ b/services/tests/displayservicetests/Android.bp
@@ -7,19 +7,12 @@ package {
default_applicable_licenses: ["frameworks_base_license"],
}
-// Include all test java files.
-filegroup {
- name: "displayservicetests-sources",
- srcs: [
- "src/**/*.java",
- ],
-}
-
android_test {
name: "DisplayServiceTests",
srcs: [
"src/**/*.java",
+ ":extended-mockito-rule-sources",
],
libs: [
@@ -28,14 +21,15 @@ android_test {
static_libs: [
"androidx.test.ext.junit",
- "display-core-libs",
"frameworks-base-testutils",
"junit",
"junit-params",
+ "mockingservicestests-utils-mockito",
"platform-compat-test-rules",
"platform-test-annotations",
"services.core",
"servicestests-utils",
+ "testables",
],
defaults: [
@@ -56,10 +50,3 @@ android_test {
enabled: false,
},
}
-
-java_library {
- name: "display-core-libs",
- srcs: [
- "src/com/android/server/display/TestUtils.java",
- ],
-}
diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml
index d2bd10dd18dc..55fde00a05db 100644
--- a/services/tests/displayservicetests/AndroidManifest.xml
+++ b/services/tests/displayservicetests/AndroidManifest.xml
@@ -17,10 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.displayservicetests">
- <!--
- Insert permissions here. eg:
- <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
- -->
+ <!-- Permissions -->
<uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
<uses-permission android:name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
@@ -32,6 +29,10 @@
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <!-- Permissions needed for DisplayTransformManagerTest -->
+ <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
+ <uses-permission android:name="android.permission.HARDWARE_TEST"/>
+
<application android:debuggable="true"
android:testOnly="true">
<uses-library android:name="android.test.mock" android:required="true" />
diff --git a/services/tests/displayservicetests/TEST_MAPPING b/services/tests/displayservicetests/TEST_MAPPING
index d8655194ab0f..477860d3a1c5 100644
--- a/services/tests/displayservicetests/TEST_MAPPING
+++ b/services/tests/displayservicetests/TEST_MAPPING
@@ -1,13 +1,7 @@
{
- "presubmit": [
+ "imports": [
{
- "name": "DisplayServiceTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
+ "path": "frameworks/base/services/core/java/com/android/server/display"
}
]
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/BrightnessSynchronizerTest.java b/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java
index 2fd6e5fb6892..2fd6e5fb6892 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/BrightnessSynchronizerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/BrightnessSynchronizerTest.java
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DensityMappingTest.java b/services/tests/displayservicetests/src/com/android/server/display/DensityMappingTest.java
index ae7a2a40d195..ae7a2a40d195 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DensityMappingTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DensityMappingTest.java
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
index 95c62aeec19a..7e69357ed006 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayBrightnessStateTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
@@ -86,7 +86,9 @@ public class DisplayBrightnessStateTest {
.append("\n brightnessReason:")
.append(displayBrightnessState.getBrightnessReason())
.append("\n shouldUseAutoBrightness:")
- .append(displayBrightnessState.getShouldUseAutoBrightness());
+ .append(displayBrightnessState.getShouldUseAutoBrightness())
+ .append("\n isSlowChange:")
+ .append(displayBrightnessState.isSlowChange());
return sb.toString();
}
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
index 56f650ee9084..c39bb56e7ba1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -50,6 +50,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
@@ -144,11 +145,12 @@ public final class DisplayPowerController2Test {
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- // Put the system into manual brightness by default, just to minimize unexpected events and
- // have a consistent starting state
+ // Set some settings to minimize unexpected events and have a consistent starting state
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index e2aeea3bedba..0544376959ee 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -50,6 +50,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
@@ -144,12 +145,12 @@ public final class DisplayPowerControllerTest {
mTestLooper = new TestLooper(mClock::now);
mHandler = new Handler(mTestLooper.getLooper());
- // Put the system into manual brightness by default, just to minimize unexpected events and
- // have a consistent starting state
+ // Set some settings to minimize unexpected events and have a consistent starting state
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
-
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT);
addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class,
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
index 534a708af3c7..534a708af3c7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerProximityStateControllerTest.java
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
index d9fbba5b4274..7e7ccf733876 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java
@@ -17,35 +17,69 @@
package com.android.server.display;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
public class HighBrightnessModeMetadataMapperTest {
+ @Mock
+ private LogicalDisplay mDisplayMock;
+
+ @Mock
+ private DisplayDevice mDeviceMock;
+
+ @Mock
+ private DisplayDeviceConfig mDdcMock;
+
+ @Mock
+ private DisplayDeviceConfig.HighBrightnessModeData mHbmDataMock;
+
private HighBrightnessModeMetadataMapper mHighBrightnessModeMetadataMapper;
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDeviceMock);
+ when(mDeviceMock.getDisplayDeviceConfig()).thenReturn(mDdcMock);
+ when(mDdcMock.getHighBrightnessModeData()).thenReturn(mHbmDataMock);
mHighBrightnessModeMetadataMapper = new HighBrightnessModeMetadataMapper();
}
@Test
- public void testGetHighBrightnessModeMetadata() {
- // Display device is null
- final LogicalDisplay display = mock(LogicalDisplay.class);
- when(display.getPrimaryDisplayDeviceLocked()).thenReturn(null);
- assertNull(mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display));
-
- // No HBM metadata stored for this display yet
- final DisplayDevice device = mock(DisplayDevice.class);
- when(display.getPrimaryDisplayDeviceLocked()).thenReturn(device);
+ public void testGetHighBrightnessModeMetadata_NoDisplayDevice() {
+ when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(null);
+ assertNull(mHighBrightnessModeMetadataMapper
+ .getHighBrightnessModeMetadataLocked(mDisplayMock));
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_NoHBMData() {
+ when(mDdcMock.getHighBrightnessModeData()).thenReturn(null);
+ assertNull(mHighBrightnessModeMetadataMapper
+ .getHighBrightnessModeMetadataLocked(mDisplayMock));
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_NewDisplay() {
HighBrightnessModeMetadata hbmMetadata =
- mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+ assertNotNull(hbmMetadata);
+ assertTrue(hbmMetadata.getHbmEventQueue().isEmpty());
+ assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0);
+ }
+
+ @Test
+ public void testGetHighBrightnessModeMetadata_Modify() {
+ HighBrightnessModeMetadata hbmMetadata =
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+ assertNotNull(hbmMetadata);
assertTrue(hbmMetadata.getHbmEventQueue().isEmpty());
assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0);
@@ -55,8 +89,10 @@ public class HighBrightnessModeMetadataMapperTest {
long setTime = 300;
hbmMetadata.addHbmEvent(new HbmEvent(startTimeMillis, endTimeMillis));
hbmMetadata.setRunningStartTimeMillis(setTime);
+
hbmMetadata =
- mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
+ mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock);
+
assertEquals(1, hbmMetadata.getHbmEventQueue().size());
assertEquals(startTimeMillis,
hbmMetadata.getHbmEventQueue().getFirst().getStartTimeMillis());
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index aa0a2fea1a5a..aa0a2fea1a5a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java
index c23d4b19cd3a..c23d4b19cd3a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/WakelockControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/WakelockControllerTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
index 081f19d19f75..d8569f75996f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
@@ -21,8 +21,8 @@ import static org.junit.Assert.assertEquals;
import android.hardware.display.DisplayManagerInternal;
import android.view.Display;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.server.display.DisplayBrightnessState;
import com.android.server.display.brightness.BrightnessReason;
@@ -46,7 +46,8 @@ public class FollowerBrightnessStrategyTest {
DisplayManagerInternal.DisplayPowerRequest
displayPowerRequest = new DisplayManagerInternal.DisplayPowerRequest();
float brightnessToFollow = 0.2f;
- mFollowerBrightnessStrategy.setBrightnessToFollow(brightnessToFollow);
+ boolean slowChange = true;
+ mFollowerBrightnessStrategy.setBrightnessToFollow(brightnessToFollow, slowChange);
BrightnessReason brightnessReason = new BrightnessReason();
brightnessReason.setReason(BrightnessReason.REASON_FOLLOWER);
DisplayBrightnessState expectedDisplayBrightnessState =
@@ -55,6 +56,7 @@ public class FollowerBrightnessStrategyTest {
.setBrightnessReason(brightnessReason)
.setSdrBrightness(brightnessToFollow)
.setDisplayBrightnessStrategyName(mFollowerBrightnessStrategy.getName())
+ .setIsSlowChange(slowChange)
.build();
DisplayBrightnessState updatedDisplayBrightnessState =
mFollowerBrightnessStrategy.updateBrightness(displayPowerRequest);
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayTransformManagerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java
index a785300e98a3..a785300e98a3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayTransformManagerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayTransformManagerTest.java
diff --git a/services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
index e0bef1a83821..c280349a0559 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
@@ -16,22 +16,49 @@
package com.android.server.display.color;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import android.content.Context;
+import android.content.res.Resources;
import android.hardware.display.DisplayManagerInternal;
+import android.os.Binder;
+import android.os.IBinder;
+import android.view.SurfaceControl;
import androidx.test.InstrumentationRegistry;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.R;
+
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
import java.util.Arrays;
public class DisplayWhiteBalanceTintControllerTest {
+ @Mock
+ private Context mMockedContext;
+ @Mock
+ private Resources mMockedResources;
+ @Mock
+ private DisplayManagerInternal mDisplayManagerInternal;
- private DisplayWhiteBalanceTintController mDisplayWhiteBalanceTintController;
+ private MockitoSession mSession;
+ private Resources mResources;
+ IBinder mDisplayToken;
+ DisplayWhiteBalanceTintController mDisplayWhiteBalanceTintController;
@Before
public void setUp() {
@@ -40,6 +67,47 @@ public class DisplayWhiteBalanceTintControllerTest {
new DisplayWhiteBalanceTintController(displayManagerInternal);
mDisplayWhiteBalanceTintController.setUp(InstrumentationRegistry.getContext(), true);
mDisplayWhiteBalanceTintController.setActivated(true);
+
+ mSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .mockStatic(SurfaceControl.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+
+ mResources = InstrumentationRegistry.getContext().getResources();
+ // These Resources are common to all tests.
+ doReturn(4000)
+ .when(mMockedResources)
+ .getInteger(R.integer.config_displayWhiteBalanceColorTemperatureMin);
+ doReturn(8000)
+ .when(mMockedResources)
+ .getInteger(R.integer.config_displayWhiteBalanceColorTemperatureMax);
+ doReturn(6500)
+ .when(mMockedResources)
+ .getInteger(R.integer.config_displayWhiteBalanceColorTemperatureDefault);
+ doReturn(new String[] {"0.950456", "1.000000", "1.089058"})
+ .when(mMockedResources)
+ .getStringArray(R.array.config_displayWhiteBalanceDisplayNominalWhite);
+ doReturn(6500)
+ .when(mMockedResources)
+ .getInteger(R.integer.config_displayWhiteBalanceDisplayNominalWhiteCct);
+ doReturn(new int[] {0})
+ .when(mMockedResources)
+ .getIntArray(R.array.config_displayWhiteBalanceDisplaySteps);
+ doReturn(new int[] {20})
+ .when(mMockedResources)
+ .getIntArray(R.array.config_displayWhiteBalanceDisplayRangeMinimums);
+
+ doReturn(mMockedResources).when(mMockedContext).getResources();
+
+ mDisplayToken = new Binder();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mSession != null) {
+ mSession.finishMocking();
+ }
}
@Test
@@ -98,4 +166,204 @@ public class DisplayWhiteBalanceTintControllerTest {
})
).isTrue();
}
+
+
+ /**
+ * Setup should succeed when SurfaceControl setup results in a valid color transform.
+ */
+ @Test
+ public void displayWhiteBalance_setupWithSurfaceControl() {
+ // Make SurfaceControl return sRGB primaries
+ SurfaceControl.DisplayPrimaries displayPrimaries = new SurfaceControl.DisplayPrimaries();
+ displayPrimaries.red = new SurfaceControl.CieXyz();
+ displayPrimaries.red.X = 0.412315f;
+ displayPrimaries.red.Y = 0.212600f;
+ displayPrimaries.red.Z = 0.019327f;
+ displayPrimaries.green = new SurfaceControl.CieXyz();
+ displayPrimaries.green.X = 0.357600f;
+ displayPrimaries.green.Y = 0.715200f;
+ displayPrimaries.green.Z = 0.119200f;
+ displayPrimaries.blue = new SurfaceControl.CieXyz();
+ displayPrimaries.blue.X = 0.180500f;
+ displayPrimaries.blue.Y = 0.072200f;
+ displayPrimaries.blue.Z = 0.950633f;
+ displayPrimaries.white = new SurfaceControl.CieXyz();
+ displayPrimaries.white.X = 0.950456f;
+ displayPrimaries.white.Y = 1.000000f;
+ displayPrimaries.white.Z = 1.089058f;
+ when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
+ .thenReturn(displayPrimaries);
+
+ setUpTintController();
+ assertWithMessage("Setup with valid SurfaceControl failed")
+ .that(mDisplayWhiteBalanceTintController.mSetUp)
+ .isTrue();
+ }
+
+ /**
+ * Setup should fail when SurfaceControl setup results in an invalid color transform.
+ */
+ @Test
+ public void displayWhiteBalance_setupWithInvalidSurfaceControlData() {
+ // Make SurfaceControl return invalid display primaries
+ SurfaceControl.DisplayPrimaries displayPrimaries = new SurfaceControl.DisplayPrimaries();
+ displayPrimaries.red = new SurfaceControl.CieXyz();
+ displayPrimaries.green = new SurfaceControl.CieXyz();
+ displayPrimaries.blue = new SurfaceControl.CieXyz();
+ displayPrimaries.white = new SurfaceControl.CieXyz();
+ when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
+ .thenReturn(displayPrimaries);
+
+ setUpTintController();
+ assertWithMessage("Setup with invalid SurfaceControl succeeded")
+ .that(mDisplayWhiteBalanceTintController.mSetUp)
+ .isFalse();
+ }
+
+ /**
+ * Setup should succeed when SurfaceControl setup fails and Resources result in a valid color
+ * transform.
+ */
+ @Test
+ public void displayWhiteBalance_setupWithResources() {
+ // Use default (valid) Resources
+ doReturn(mResources.getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries))
+ .when(mMockedResources)
+ .getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries);
+ // Make SurfaceControl setup fail
+ when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)).thenReturn(null);
+
+ setUpTintController();
+ assertWithMessage("Setup with valid Resources failed")
+ .that(mDisplayWhiteBalanceTintController.mSetUp)
+ .isTrue();
+ }
+
+ /**
+ * Setup should fail when SurfaceControl setup fails and Resources result in an invalid color
+ * transform.
+ */
+ @Test
+ public void displayWhiteBalance_setupWithInvalidResources() {
+ // Use Resources with invalid color data
+ doReturn(new String[] {
+ "0", "0", "0", // Red X, Y, Z
+ "0", "0", "0", // Green X, Y, Z
+ "0", "0", "0", // Blue X, Y, Z
+ "0", "0", "0", // White X, Y, Z
+ })
+ .when(mMockedResources)
+ .getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries);
+ // Make SurfaceControl setup fail
+ when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)).thenReturn(null);
+
+ setUpTintController();
+ assertWithMessage("Setup with invalid Resources succeeded")
+ .that(mDisplayWhiteBalanceTintController.mSetUp)
+ .isFalse();
+ }
+
+ /**
+ * Matrix should match the precalculated one for given cct and display primaries.
+ */
+ @Test
+ public void displayWhiteBalance_getAndSetMatrix_validateTransformMatrix() {
+ SurfaceControl.DisplayPrimaries displayPrimaries = new SurfaceControl.DisplayPrimaries();
+ displayPrimaries.red = new SurfaceControl.CieXyz();
+ displayPrimaries.red.X = 0.412315f;
+ displayPrimaries.red.Y = 0.212600f;
+ displayPrimaries.red.Z = 0.019327f;
+ displayPrimaries.green = new SurfaceControl.CieXyz();
+ displayPrimaries.green.X = 0.357600f;
+ displayPrimaries.green.Y = 0.715200f;
+ displayPrimaries.green.Z = 0.119200f;
+ displayPrimaries.blue = new SurfaceControl.CieXyz();
+ displayPrimaries.blue.X = 0.180500f;
+ displayPrimaries.blue.Y = 0.072200f;
+ displayPrimaries.blue.Z = 0.950633f;
+ displayPrimaries.white = new SurfaceControl.CieXyz();
+ displayPrimaries.white.X = 0.950456f;
+ displayPrimaries.white.Y = 1.000000f;
+ displayPrimaries.white.Z = 1.089058f;
+ when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
+ .thenReturn(displayPrimaries);
+
+ setUpTintController();
+ assertWithMessage("Setup with valid SurfaceControl failed")
+ .that(mDisplayWhiteBalanceTintController.mSetUp)
+ .isTrue();
+
+ final int cct = 6500;
+ mDisplayWhiteBalanceTintController.setMatrix(cct);
+ mDisplayWhiteBalanceTintController.setAppliedCct(
+ mDisplayWhiteBalanceTintController.getTargetCct());
+
+ assertWithMessage("Failed to set temperature")
+ .that(mDisplayWhiteBalanceTintController.mCurrentColorTemperature)
+ .isEqualTo(cct);
+ float[] matrixDwb = mDisplayWhiteBalanceTintController.getMatrix();
+ final float[] expectedMatrixDwb = {
+ 0.971848f, -0.001421f, 0.000491f, 0.0f,
+ 0.028193f, 0.945798f, 0.003207f, 0.0f,
+ -0.000042f, -0.000989f, 0.988659f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f
+ };
+ assertArrayEquals("Unexpected DWB matrix", expectedMatrixDwb, matrixDwb,
+ 1e-6f /* tolerance */);
+ }
+
+ /**
+ * Matrix should match the precalculated one for given cct and display primaries.
+ */
+ @Test
+ public void displayWhiteBalance_targetApplied_validateTransformMatrix() {
+ SurfaceControl.DisplayPrimaries displayPrimaries = new SurfaceControl.DisplayPrimaries();
+ displayPrimaries.red = new SurfaceControl.CieXyz();
+ displayPrimaries.red.X = 0.412315f;
+ displayPrimaries.red.Y = 0.212600f;
+ displayPrimaries.red.Z = 0.019327f;
+ displayPrimaries.green = new SurfaceControl.CieXyz();
+ displayPrimaries.green.X = 0.357600f;
+ displayPrimaries.green.Y = 0.715200f;
+ displayPrimaries.green.Z = 0.119200f;
+ displayPrimaries.blue = new SurfaceControl.CieXyz();
+ displayPrimaries.blue.X = 0.180500f;
+ displayPrimaries.blue.Y = 0.072200f;
+ displayPrimaries.blue.Z = 0.950633f;
+ displayPrimaries.white = new SurfaceControl.CieXyz();
+ displayPrimaries.white.X = 0.950456f;
+ displayPrimaries.white.Y = 1.000000f;
+ displayPrimaries.white.Z = 1.089058f;
+ when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
+ .thenReturn(displayPrimaries);
+
+ setUpTintController();
+ assertWithMessage("Setup with valid SurfaceControl failed")
+ .that(mDisplayWhiteBalanceTintController.mSetUp)
+ .isTrue();
+
+ final int cct = 6500;
+ mDisplayWhiteBalanceTintController.setTargetCct(cct);
+ final float[] matrixDwb = mDisplayWhiteBalanceTintController.computeMatrixForCct(cct);
+ mDisplayWhiteBalanceTintController.setAppliedCct(cct);
+
+ assertWithMessage("Failed to set temperature")
+ .that(mDisplayWhiteBalanceTintController.mCurrentColorTemperature)
+ .isEqualTo(cct);
+ final float[] expectedMatrixDwb = {
+ 0.971848f, -0.001421f, 0.000491f, 0.0f,
+ 0.028193f, 0.945798f, 0.003207f, 0.0f,
+ -0.000042f, -0.000989f, 0.988659f, 0.0f,
+ 0.0f, 0.0f, 0.0f, 1.0f
+ };
+ assertArrayEquals("Unexpected DWB matrix", expectedMatrixDwb, matrixDwb,
+ 1e-6f /* tolerance */);
+ }
+
+ private void setUpTintController() {
+ mDisplayWhiteBalanceTintController = new DisplayWhiteBalanceTintController(
+ mDisplayManagerInternal);
+ mDisplayWhiteBalanceTintController.setUp(mMockedContext, true);
+ mDisplayWhiteBalanceTintController.setActivated(true);
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
index 880501f39ac2..880501f39ac2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 101498aef183..bde02e8f54cc 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -115,3 +115,14 @@ java_library {
"android.test.runner",
],
}
+
+filegroup {
+ name: "extended-mockito-rule-sources",
+ srcs: [
+ "src/com/android/server/ExtendedMockitoRule.java",
+ "src/com/android/server/Visitor.java",
+ ],
+ visibility: [
+ "//frameworks/base/services/tests/displayservicetests",
+ ],
+}
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 72c5333e0a02..410ae35aa790 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -328,7 +328,7 @@ public class BroadcastQueueTest {
eq(ActivityManager.PROCESS_STATE_LAST_ACTIVITY), any());
mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
- mConstants.TIMEOUT = 100;
+ mConstants.TIMEOUT = 200;
mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 1f4563fb2682..9545a8a4544b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -68,7 +68,6 @@ import static com.android.server.am.ProcessList.UNKNOWN_ADJ;
import static com.android.server.am.ProcessList.VISIBLE_APP_ADJ;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.answer;
@@ -2522,28 +2521,6 @@ public class MockingOomAdjusterTests {
@SuppressWarnings("GuardedBy")
@Test
- public void testUpdateOomAdj_DoOne_AboveClient_SameProcess() {
- ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
- MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
- doReturn(PROCESS_STATE_TOP).when(sService.mAtmInternal).getTopProcessState();
- doReturn(app).when(sService).getTopApp();
- sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
- sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE);
-
- assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
-
- // Simulate binding to a service in the same process using BIND_ABOVE_CLIENT and
- // verify that its OOM adjustment level is unaffected.
- bindService(app, app, null, Context.BIND_ABOVE_CLIENT, mock(IBinder.class));
- app.mServices.updateHasAboveClientLocked();
- assertFalse(app.mServices.hasAboveClient());
-
- sService.mOomAdjuster.updateOomAdjLocked(app, OOM_ADJ_REASON_NONE);
- assertEquals(FOREGROUND_APP_ADJ, app.mState.getSetAdj());
- }
-
- @SuppressWarnings("GuardedBy")
- @Test
public void testUpdateOomAdj_DoAll_Side_Cycle() {
final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, false));
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/OWNERS b/services/tests/mockingservicestests/src/com/android/server/display/OWNERS
deleted file mode 100644
index 6ce1ee4d3de2..000000000000
--- a/services/tests/mockingservicestests/src/com/android/server/display/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-include /services/core/java/com/android/server/display/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
deleted file mode 100644
index 3faf394fc534..000000000000
--- a/services/tests/mockingservicestests/src/com/android/server/display/color/DisplayWhiteBalanceTintControllerTest.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.color;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.hardware.display.DisplayManagerInternal;
-import android.os.Binder;
-import android.os.IBinder;
-import android.view.SurfaceControl;
-import android.view.SurfaceControl.CieXyz;
-import android.view.SurfaceControl.DisplayPrimaries;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
-import com.android.internal.R;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@RunWith(AndroidJUnit4.class)
-public class DisplayWhiteBalanceTintControllerTest {
- @Mock
- private Context mMockedContext;
- @Mock
- private Resources mMockedResources;
- @Mock
- private DisplayManagerInternal mDisplayManagerInternal;
-
- private MockitoSession mSession;
- private Resources mResources;
- IBinder mDisplayToken;
- DisplayWhiteBalanceTintController mDisplayWhiteBalanceTintController;
-
- @Before
- public void setUp() {
- mSession = ExtendedMockito.mockitoSession()
- .initMocks(this)
- .mockStatic(SurfaceControl.class)
- .strictness(Strictness.LENIENT)
- .startMocking();
-
- mResources = InstrumentationRegistry.getContext().getResources();
- // These Resources are common to all tests.
- doReturn(4000)
- .when(mMockedResources)
- .getInteger(R.integer.config_displayWhiteBalanceColorTemperatureMin);
- doReturn(8000)
- .when(mMockedResources)
- .getInteger(R.integer.config_displayWhiteBalanceColorTemperatureMax);
- doReturn(6500)
- .when(mMockedResources)
- .getInteger(R.integer.config_displayWhiteBalanceColorTemperatureDefault);
- doReturn(new String[] {"0.950456", "1.000000", "1.089058"})
- .when(mMockedResources)
- .getStringArray(R.array.config_displayWhiteBalanceDisplayNominalWhite);
- doReturn(6500)
- .when(mMockedResources)
- .getInteger(R.integer.config_displayWhiteBalanceDisplayNominalWhiteCct);
- doReturn(new int[] {0})
- .when(mMockedResources)
- .getIntArray(R.array.config_displayWhiteBalanceDisplaySteps);
- doReturn(new int[] {20})
- .when(mMockedResources)
- .getIntArray(R.array.config_displayWhiteBalanceDisplayRangeMinimums);
-
- doReturn(mMockedResources).when(mMockedContext).getResources();
-
- mDisplayToken = new Binder();
- }
-
- @After
- public void tearDown() throws Exception {
- if (mSession != null) {
- mSession.finishMocking();
- }
- }
-
- /**
- * Setup should succeed when SurfaceControl setup results in a valid color transform.
- */
- @Test
- public void displayWhiteBalance_setupWithSurfaceControl() {
- // Make SurfaceControl return sRGB primaries
- DisplayPrimaries displayPrimaries = new DisplayPrimaries();
- displayPrimaries.red = new CieXyz();
- displayPrimaries.red.X = 0.412315f;
- displayPrimaries.red.Y = 0.212600f;
- displayPrimaries.red.Z = 0.019327f;
- displayPrimaries.green = new CieXyz();
- displayPrimaries.green.X = 0.357600f;
- displayPrimaries.green.Y = 0.715200f;
- displayPrimaries.green.Z = 0.119200f;
- displayPrimaries.blue = new CieXyz();
- displayPrimaries.blue.X = 0.180500f;
- displayPrimaries.blue.Y = 0.072200f;
- displayPrimaries.blue.Z = 0.950633f;
- displayPrimaries.white = new CieXyz();
- displayPrimaries.white.X = 0.950456f;
- displayPrimaries.white.Y = 1.000000f;
- displayPrimaries.white.Z = 1.089058f;
- when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
- .thenReturn(displayPrimaries);
-
- setUpTintController();
- assertWithMessage("Setup with valid SurfaceControl failed")
- .that(mDisplayWhiteBalanceTintController.mSetUp)
- .isTrue();
- }
-
- /**
- * Setup should fail when SurfaceControl setup results in an invalid color transform.
- */
- @Test
- public void displayWhiteBalance_setupWithInvalidSurfaceControlData() {
- // Make SurfaceControl return invalid display primaries
- DisplayPrimaries displayPrimaries = new DisplayPrimaries();
- displayPrimaries.red = new CieXyz();
- displayPrimaries.green = new CieXyz();
- displayPrimaries.blue = new CieXyz();
- displayPrimaries.white = new CieXyz();
- when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
- .thenReturn(displayPrimaries);
-
- setUpTintController();
- assertWithMessage("Setup with invalid SurfaceControl succeeded")
- .that(mDisplayWhiteBalanceTintController.mSetUp)
- .isFalse();
- }
-
- /**
- * Setup should succeed when SurfaceControl setup fails and Resources result in a valid color
- * transform.
- */
- @Test
- public void displayWhiteBalance_setupWithResources() {
- // Use default (valid) Resources
- doReturn(mResources.getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries))
- .when(mMockedResources)
- .getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries);
- // Make SurfaceControl setup fail
- when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)).thenReturn(null);
-
- setUpTintController();
- assertWithMessage("Setup with valid Resources failed")
- .that(mDisplayWhiteBalanceTintController.mSetUp)
- .isTrue();
- }
-
- /**
- * Setup should fail when SurfaceControl setup fails and Resources result in an invalid color
- * transform.
- */
- @Test
- public void displayWhiteBalance_setupWithInvalidResources() {
- // Use Resources with invalid color data
- doReturn(new String[] {
- "0", "0", "0", // Red X, Y, Z
- "0", "0", "0", // Green X, Y, Z
- "0", "0", "0", // Blue X, Y, Z
- "0", "0", "0", // White X, Y, Z
- })
- .when(mMockedResources)
- .getStringArray(R.array.config_displayWhiteBalanceDisplayPrimaries);
- // Make SurfaceControl setup fail
- when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY)).thenReturn(null);
-
- setUpTintController();
- assertWithMessage("Setup with invalid Resources succeeded")
- .that(mDisplayWhiteBalanceTintController.mSetUp)
- .isFalse();
- }
-
- /**
- * Matrix should match the precalculated one for given cct and display primaries.
- */
- @Test
- public void displayWhiteBalance_getAndSetMatrix_validateTransformMatrix() {
- DisplayPrimaries displayPrimaries = new DisplayPrimaries();
- displayPrimaries.red = new CieXyz();
- displayPrimaries.red.X = 0.412315f;
- displayPrimaries.red.Y = 0.212600f;
- displayPrimaries.red.Z = 0.019327f;
- displayPrimaries.green = new CieXyz();
- displayPrimaries.green.X = 0.357600f;
- displayPrimaries.green.Y = 0.715200f;
- displayPrimaries.green.Z = 0.119200f;
- displayPrimaries.blue = new CieXyz();
- displayPrimaries.blue.X = 0.180500f;
- displayPrimaries.blue.Y = 0.072200f;
- displayPrimaries.blue.Z = 0.950633f;
- displayPrimaries.white = new CieXyz();
- displayPrimaries.white.X = 0.950456f;
- displayPrimaries.white.Y = 1.000000f;
- displayPrimaries.white.Z = 1.089058f;
- when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
- .thenReturn(displayPrimaries);
-
- setUpTintController();
- assertWithMessage("Setup with valid SurfaceControl failed")
- .that(mDisplayWhiteBalanceTintController.mSetUp)
- .isTrue();
-
- final int cct = 6500;
- mDisplayWhiteBalanceTintController.setMatrix(cct);
- mDisplayWhiteBalanceTintController.setAppliedCct(
- mDisplayWhiteBalanceTintController.getTargetCct());
-
- assertWithMessage("Failed to set temperature")
- .that(mDisplayWhiteBalanceTintController.mCurrentColorTemperature)
- .isEqualTo(cct);
- float[] matrixDwb = mDisplayWhiteBalanceTintController.getMatrix();
- final float[] expectedMatrixDwb = {
- 0.971848f, -0.001421f, 0.000491f, 0.0f,
- 0.028193f, 0.945798f, 0.003207f, 0.0f,
- -0.000042f, -0.000989f, 0.988659f, 0.0f,
- 0.0f, 0.0f, 0.0f, 1.0f
- };
- assertArrayEquals("Unexpected DWB matrix", expectedMatrixDwb, matrixDwb,
- 1e-6f /* tolerance */);
- }
-
- /**
- * Matrix should match the precalculated one for given cct and display primaries.
- */
- @Test
- public void displayWhiteBalance_targetApplied_validateTransformMatrix() {
- DisplayPrimaries displayPrimaries = new DisplayPrimaries();
- displayPrimaries.red = new CieXyz();
- displayPrimaries.red.X = 0.412315f;
- displayPrimaries.red.Y = 0.212600f;
- displayPrimaries.red.Z = 0.019327f;
- displayPrimaries.green = new CieXyz();
- displayPrimaries.green.X = 0.357600f;
- displayPrimaries.green.Y = 0.715200f;
- displayPrimaries.green.Z = 0.119200f;
- displayPrimaries.blue = new CieXyz();
- displayPrimaries.blue.X = 0.180500f;
- displayPrimaries.blue.Y = 0.072200f;
- displayPrimaries.blue.Z = 0.950633f;
- displayPrimaries.white = new CieXyz();
- displayPrimaries.white.X = 0.950456f;
- displayPrimaries.white.Y = 1.000000f;
- displayPrimaries.white.Z = 1.089058f;
- when(mDisplayManagerInternal.getDisplayNativePrimaries(DEFAULT_DISPLAY))
- .thenReturn(displayPrimaries);
-
- setUpTintController();
- assertWithMessage("Setup with valid SurfaceControl failed")
- .that(mDisplayWhiteBalanceTintController.mSetUp)
- .isTrue();
-
- final int cct = 6500;
- mDisplayWhiteBalanceTintController.setTargetCct(cct);
- final float[] matrixDwb = mDisplayWhiteBalanceTintController.computeMatrixForCct(cct);
- mDisplayWhiteBalanceTintController.setAppliedCct(cct);
-
- assertWithMessage("Failed to set temperature")
- .that(mDisplayWhiteBalanceTintController.mCurrentColorTemperature)
- .isEqualTo(cct);
- final float[] expectedMatrixDwb = {
- 0.971848f, -0.001421f, 0.000491f, 0.0f,
- 0.028193f, 0.945798f, 0.003207f, 0.0f,
- -0.000042f, -0.000989f, 0.988659f, 0.0f,
- 0.0f, 0.0f, 0.0f, 1.0f
- };
- assertArrayEquals("Unexpected DWB matrix", expectedMatrixDwb, matrixDwb,
- 1e-6f /* tolerance */);
- }
-
- private void setUpTintController() {
- mDisplayWhiteBalanceTintController = new DisplayWhiteBalanceTintController(
- mDisplayManagerInternal);
- mDisplayWhiteBalanceTintController.setUp(mMockedContext, true);
- mDisplayWhiteBalanceTintController.setActivated(true);
- }
-}
diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
index e4571194b37d..e7777f75b6df 100644
--- a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java
@@ -42,6 +42,7 @@ import androidx.test.filters.SmallTest;
import com.android.server.LocalServices;
import com.android.server.contentprotection.ContentProtectionBlocklistManager;
+import com.android.server.contentprotection.ContentProtectionConsentManager;
import com.android.server.contentprotection.RemoteContentProtectionService;
import com.android.server.pm.UserManagerInternal;
@@ -91,6 +92,8 @@ public class ContentCaptureManagerServiceTest {
@Mock private RemoteContentProtectionService mMockRemoteContentProtectionService;
+ @Mock private ContentProtectionConsentManager mMockContentProtectionConsentManager;
+
private boolean mDevCfgEnableContentProtectionReceiver;
private int mContentProtectionBlocklistManagersCreated;
@@ -99,6 +102,8 @@ public class ContentCaptureManagerServiceTest {
private int mRemoteContentProtectionServicesCreated;
+ private int mContentProtectionConsentManagersCreated;
+
private String mConfigDefaultContentProtectionService = COMPONENT_NAME.flattenToString();
private boolean mContentProtectionServiceInfoConstructorShouldThrow;
@@ -114,43 +119,51 @@ public class ContentCaptureManagerServiceTest {
}
@Test
- public void constructor_contentProtection_flagDisabled_noBlocklistManager() {
+ public void constructor_contentProtection_flagDisabled_noManagers() {
assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0);
assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+ assertThat(mContentProtectionConsentManagersCreated).isEqualTo(0);
verifyZeroInteractions(mMockContentProtectionBlocklistManager);
+ verifyZeroInteractions(mMockContentProtectionConsentManager);
}
@Test
- public void constructor_contentProtection_componentNameNull_noBlocklistManager() {
+ public void constructor_contentProtection_componentNameNull_noManagers() {
mConfigDefaultContentProtectionService = null;
mContentCaptureManagerService = new TestContentCaptureManagerService();
assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0);
assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+ assertThat(mContentProtectionConsentManagersCreated).isEqualTo(0);
verifyZeroInteractions(mMockContentProtectionBlocklistManager);
+ verifyZeroInteractions(mMockContentProtectionConsentManager);
}
@Test
- public void constructor_contentProtection_componentNameBlank_noBlocklistManager() {
+ public void constructor_contentProtection_componentNameBlank_noManagers() {
mConfigDefaultContentProtectionService = " ";
mContentCaptureManagerService = new TestContentCaptureManagerService();
assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(0);
assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
+ assertThat(mContentProtectionConsentManagersCreated).isEqualTo(0);
verifyZeroInteractions(mMockContentProtectionBlocklistManager);
+ verifyZeroInteractions(mMockContentProtectionConsentManager);
}
@Test
- public void constructor_contentProtection_enabled_createsBlocklistManager() {
+ public void constructor_contentProtection_enabled_createsManagers() {
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
assertThat(mContentProtectionBlocklistManagersCreated).isEqualTo(1);
+ assertThat(mContentProtectionConsentManagersCreated).isEqualTo(1);
assertThat(mContentProtectionServiceInfosCreated).isEqualTo(0);
verify(mMockContentProtectionBlocklistManager).updateBlocklist(anyInt());
+ verifyZeroInteractions(mMockContentProtectionConsentManager);
}
@Test
@@ -175,11 +188,13 @@ public class ContentCaptureManagerServiceTest {
USER_ID, PACKAGE_NAME);
assertThat(actual).isNull();
- verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME);
+ verify(mMockContentProtectionConsentManager).isConsentGranted(USER_ID);
+ verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
}
@Test
public void getOptions_contentCaptureDisabled_contentProtectionEnabled() {
+ when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true);
when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
@@ -211,11 +226,13 @@ public class ContentCaptureManagerServiceTest {
assertThat(actual.contentProtectionOptions).isNotNull();
assertThat(actual.contentProtectionOptions.enableReceiver).isFalse();
assertThat(actual.whitelistedComponents).isNull();
- verify(mMockContentProtectionBlocklistManager).isAllowed(PACKAGE_NAME);
+ verify(mMockContentProtectionConsentManager).isConsentGranted(USER_ID);
+ verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
}
@Test
public void getOptions_contentCaptureEnabled_contentProtectionEnabled() {
+ when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true);
when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
@@ -234,7 +251,22 @@ public class ContentCaptureManagerServiceTest {
}
@Test
+ public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionNotGranted() {
+ mDevCfgEnableContentProtectionReceiver = true;
+ mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+ boolean actual =
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+ USER_ID, PACKAGE_NAME);
+
+ assertThat(actual).isFalse();
+ verify(mMockContentProtectionConsentManager).isConsentGranted(USER_ID);
+ verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
+ }
+
+ @Test
public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionDisabled() {
+ when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true);
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
@@ -248,6 +280,7 @@ public class ContentCaptureManagerServiceTest {
@Test
public void isWhitelisted_packageName_contentCaptureDisabled_contentProtectionEnabled() {
+ when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true);
when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
@@ -271,11 +304,27 @@ public class ContentCaptureManagerServiceTest {
USER_ID, PACKAGE_NAME);
assertThat(actual).isTrue();
+ verify(mMockContentProtectionConsentManager, never()).isConsentGranted(anyInt());
+ verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
+ }
+
+ @Test
+ public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionNotGranted() {
+ mDevCfgEnableContentProtectionReceiver = true;
+ mContentCaptureManagerService = new TestContentCaptureManagerService();
+
+ boolean actual =
+ mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
+ USER_ID, COMPONENT_NAME);
+
+ assertThat(actual).isFalse();
+ verify(mMockContentProtectionConsentManager).isConsentGranted(USER_ID);
verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
}
@Test
public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionDisabled() {
+ when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true);
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
@@ -289,6 +338,7 @@ public class ContentCaptureManagerServiceTest {
@Test
public void isWhitelisted_componentName_contentCaptureDisabled_contentProtectionEnabled() {
+ when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true);
when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true);
mDevCfgEnableContentProtectionReceiver = true;
mContentCaptureManagerService = new TestContentCaptureManagerService();
@@ -312,16 +362,18 @@ public class ContentCaptureManagerServiceTest {
USER_ID, COMPONENT_NAME);
assertThat(actual).isTrue();
+ verify(mMockContentProtectionConsentManager, never()).isConsentGranted(anyInt());
verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
}
@Test
- public void isContentProtectionReceiverEnabled_withoutBlocklistManager() {
+ public void isContentProtectionReceiverEnabled_withoutManagers() {
boolean actual =
mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted(
USER_ID, PACKAGE_NAME);
assertThat(actual).isFalse();
+ verify(mMockContentProtectionConsentManager, never()).isConsentGranted(anyInt());
verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
}
@@ -336,6 +388,7 @@ public class ContentCaptureManagerServiceTest {
USER_ID, PACKAGE_NAME);
assertThat(actual).isFalse();
+ verify(mMockContentProtectionConsentManager, never()).isConsentGranted(anyInt());
verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString());
}
@@ -423,5 +476,11 @@ public class ContentCaptureManagerServiceTest {
mRemoteContentProtectionServicesCreated++;
return mMockRemoteContentProtectionService;
}
+
+ @Override
+ protected ContentProtectionConsentManager createContentProtectionConsentManager() {
+ mContentProtectionConsentManagersCreated++;
+ return mMockContentProtectionConsentManager;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING
new file mode 100644
index 000000000000..0ffa891ce3e1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.contentcapture"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionConsentManagerTest.java b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionConsentManagerTest.java
new file mode 100644
index 000000000000..0e80bfdae291
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/ContentProtectionConsentManagerTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.contentprotection;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.testing.TestableContentResolver;
+import android.testing.TestableContext;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Test for {@link ContentProtectionConsentManager}.
+ *
+ * <p>Run with: {@code atest
+ * FrameworksServicesTests:com.android.server.contentprotection.ContentProtectionConsentManagerTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ContentProtectionConsentManagerTest {
+
+ private static final String KEY_PACKAGE_VERIFIER_USER_CONSENT = "package_verifier_user_consent";
+
+ private static final Uri URI_PACKAGE_VERIFIER_USER_CONSENT =
+ Settings.Global.getUriFor(KEY_PACKAGE_VERIFIER_USER_CONSENT);
+
+ private static final int VALUE_TRUE = 1;
+
+ private static final int VALUE_FALSE = -1;
+
+ private static final int VALUE_DEFAULT = 0;
+
+ private static final int TEST_USER_ID = 1234;
+
+ @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(ApplicationProvider.getApplicationContext());
+
+ private final TestableContentResolver mTestableContentResolver =
+ mTestableContext.getContentResolver();
+
+ @Mock private ContentResolver mMockContentResolver;
+
+ @Mock private DevicePolicyManagerInternal mMockDevicePolicyManagerInternal;
+
+ @Test
+ public void constructor_registersContentObserver() {
+ ContentProtectionConsentManager manager =
+ createContentProtectionConsentManager(mMockContentResolver);
+
+ assertThat(manager.mContentObserver).isNotNull();
+ verify(mMockContentResolver)
+ .registerContentObserver(
+ URI_PACKAGE_VERIFIER_USER_CONSENT,
+ /* notifyForDescendants= */ false,
+ manager.mContentObserver,
+ UserHandle.USER_ALL);
+ }
+
+ @Test
+ public void isConsentGranted_packageVerifierNotGranted() {
+ ContentProtectionConsentManager manager =
+ createContentProtectionConsentManager(VALUE_FALSE);
+
+ boolean actual = manager.isConsentGranted(TEST_USER_ID);
+
+ assertThat(actual).isFalse();
+ verifyZeroInteractions(mMockDevicePolicyManagerInternal);
+ }
+
+ @Test
+ public void isConsentGranted_packageVerifierGranted_userNotManaged() {
+ ContentProtectionConsentManager manager = createContentProtectionConsentManager(VALUE_TRUE);
+
+ boolean actual = manager.isConsentGranted(TEST_USER_ID);
+
+ assertThat(actual).isTrue();
+ verify(mMockDevicePolicyManagerInternal).isUserOrganizationManaged(TEST_USER_ID);
+ }
+
+ @Test
+ public void isConsentGranted_packageVerifierGranted_userManaged() {
+ when(mMockDevicePolicyManagerInternal.isUserOrganizationManaged(TEST_USER_ID))
+ .thenReturn(true);
+ ContentProtectionConsentManager manager = createContentProtectionConsentManager(VALUE_TRUE);
+
+ boolean actual = manager.isConsentGranted(TEST_USER_ID);
+
+ assertThat(actual).isFalse();
+ }
+
+ @Test
+ public void isConsentGranted_packageVerifierDefault() {
+ ContentProtectionConsentManager manager =
+ createContentProtectionConsentManager(VALUE_DEFAULT);
+
+ boolean actual = manager.isConsentGranted(TEST_USER_ID);
+
+ assertThat(actual).isFalse();
+ verifyZeroInteractions(mMockDevicePolicyManagerInternal);
+ }
+
+ @Test
+ public void contentObserver() throws Exception {
+ ContentProtectionConsentManager manager = createContentProtectionConsentManager(VALUE_TRUE);
+ boolean firstActual = manager.isConsentGranted(TEST_USER_ID);
+
+ Settings.Global.putInt(
+ mTestableContentResolver, KEY_PACKAGE_VERIFIER_USER_CONSENT, VALUE_FALSE);
+ // Observer has to be called manually, mTestableContentResolver is not propagating
+ manager.mContentObserver.onChange(
+ /* selfChange= */ false, URI_PACKAGE_VERIFIER_USER_CONSENT, TEST_USER_ID);
+ boolean secondActual = manager.isConsentGranted(TEST_USER_ID);
+
+ assertThat(firstActual).isTrue();
+ assertThat(secondActual).isFalse();
+ verify(mMockDevicePolicyManagerInternal).isUserOrganizationManaged(TEST_USER_ID);
+ }
+
+ private ContentProtectionConsentManager createContentProtectionConsentManager(
+ ContentResolver contentResolver) {
+ return new ContentProtectionConsentManager(
+ new Handler(Looper.getMainLooper()),
+ contentResolver,
+ mMockDevicePolicyManagerInternal);
+ }
+
+ private ContentProtectionConsentManager createContentProtectionConsentManager(
+ int valuePackageVerifierUserConsent) {
+ Settings.Global.putInt(
+ mTestableContentResolver,
+ KEY_PACKAGE_VERIFIER_USER_CONSENT,
+ valuePackageVerifierUserConsent);
+ return createContentProtectionConsentManager(mTestableContentResolver);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING
new file mode 100644
index 000000000000..419508ca5e17
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.contentprotection"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
index d5ad815d3cdb..b5bf1ea34a46 100644
--- a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java
@@ -16,7 +16,11 @@
package com.android.server.dreams;
+import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER;
+import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS;
+
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
@@ -32,7 +36,9 @@ import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
+import android.os.IPowerManager;
import android.os.IRemoteCallback;
+import android.os.PowerManager;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.service.dreams.IDreamService;
@@ -58,6 +64,8 @@ public class DreamControllerTest {
@Mock
private ActivityTaskManager mActivityTaskManager;
+ @Mock
+ private IPowerManager mPowerManager;
@Mock
private IBinder mIBinder;
@@ -67,6 +75,8 @@ public class DreamControllerTest {
@Captor
private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor;
@Captor
+ private ArgumentCaptor<IBinder.DeathRecipient> mDeathRecipientCaptor;
+ @Captor
private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;
private final TestLooper mLooper = new TestLooper();
@@ -90,6 +100,12 @@ public class DreamControllerTest {
when(mContext.getSystemServiceName(ActivityTaskManager.class))
.thenReturn(Context.ACTIVITY_TASK_SERVICE);
+ final PowerManager powerManager = new PowerManager(mContext, mPowerManager, null, null);
+ when(mContext.getSystemService(Context.POWER_SERVICE))
+ .thenReturn(powerManager);
+ when(mContext.getSystemServiceName(PowerManager.class))
+ .thenReturn(Context.POWER_SERVICE);
+
mToken = new Binder();
mDreamName = ComponentName.unflattenFromString("dream");
mOverlayName = ComponentName.unflattenFromString("dream_overlay");
@@ -209,9 +225,51 @@ public class DreamControllerTest {
verify(mIDreamService).detach();
}
+ @Test
+ public void serviceDisconnect_resetsScreenTimeout() throws RemoteException {
+ // Start dream.
+ mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+ 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+ ServiceConnection serviceConnection = captureServiceConnection();
+ serviceConnection.onServiceConnected(mDreamName, mIBinder);
+ mLooper.dispatchAll();
+
+ // Dream disconnects unexpectedly.
+ serviceConnection.onServiceDisconnected(mDreamName);
+ mLooper.dispatchAll();
+
+ // Power manager receives user activity signal.
+ verify(mPowerManager).userActivity(/*displayId=*/ anyInt(), /*time=*/ anyLong(),
+ eq(USER_ACTIVITY_EVENT_OTHER),
+ eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS));
+ }
+
+ @Test
+ public void binderDied_resetsScreenTimeout() throws RemoteException {
+ // Start dream.
+ mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/,
+ 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/);
+ captureServiceConnection().onServiceConnected(mDreamName, mIBinder);
+ mLooper.dispatchAll();
+
+ // Dream binder dies.
+ captureDeathRecipient().binderDied();
+ mLooper.dispatchAll();
+
+ // Power manager receives user activity signal.
+ verify(mPowerManager).userActivity(/*displayId=*/ anyInt(), /*time=*/ anyLong(),
+ eq(USER_ACTIVITY_EVENT_OTHER),
+ eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS));
+ }
+
private ServiceConnection captureServiceConnection() {
verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(),
any());
return mServiceConnectionACaptor.getValue();
}
+
+ private IBinder.DeathRecipient captureDeathRecipient() throws RemoteException {
+ verify(mIBinder).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+ return mDeathRecipientCaptor.getValue();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index 5c6164efb3b6..431d3a61de51 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -18,6 +18,8 @@ package com.android.server.power;
import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP;
import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+import static android.app.ActivityManager.PROCESS_STATE_RECEIVER;
+import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING;
import static android.os.PowerManager.USER_ACTIVITY_EVENT_BUTTON;
import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
@@ -78,6 +80,7 @@ import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.UserHandle;
import android.os.test.TestLooper;
+import android.provider.DeviceConfig;
import android.provider.Settings;
import android.service.dreams.DreamManagerInternal;
import android.sysprop.PowerProperties;
@@ -91,6 +94,7 @@ import com.android.internal.app.IBatteryStats;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.lights.LightsManager;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.power.PowerManagerService.BatteryReceiver;
@@ -159,6 +163,7 @@ public class PowerManagerServiceTest {
@Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@Mock private PowerManagerService.PermissionCheckerWrapper mPermissionCheckerWrapperMock;
@Mock private PowerManagerService.PowerPropertiesWrapper mPowerPropertiesWrapper;
+ @Mock private DeviceConfigParameterProvider mDeviceParameterProvider;
@Rule public TestRule compatChangeRule = new PlatformCompatChangeRule();
@@ -340,6 +345,11 @@ public class PowerManagerServiceTest {
PowerManagerService.PowerPropertiesWrapper createPowerPropertiesWrapper() {
return mPowerPropertiesWrapper;
}
+
+ @Override
+ DeviceConfigParameterProvider createDeviceConfigParameterProvider() {
+ return mDeviceParameterProvider;
+ }
});
return mService;
}
@@ -2680,4 +2690,197 @@ public class PowerManagerServiceTest {
verify(mNotifierMock, never()).onUserActivity(anyInt(), anyInt(), anyInt());
}
+ @Test
+ public void testFeatureEnabledProcStateUncachedToCached_fullWakeLockDisabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("fullWakeLock", PowerManager.FULL_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+
+ setCachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isTrue();
+ }
+
+ @Test
+ public void testFeatureDisabledProcStateUncachedToCached_fullWakeLockEnabled() {
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("fullWakeLock", PowerManager.FULL_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+
+ setCachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureEnabledProcStateUncachedToCached_screenBrightWakeLockDisabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenBrightWakeLock",
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+
+ setCachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isTrue();
+ }
+
+ @Test
+ public void testFeatureDisabledProcStateUncachedToCached_screenBrightWakeLockEnabled() {
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenBrightWakeLock",
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+
+ setCachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureEnabledProcStateUncachedToCached_screenDimWakeLockDisabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenDimWakeLock",
+ PowerManager.SCREEN_DIM_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+
+ setCachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isTrue();
+ }
+
+ @Test
+ public void testFeatureDisabledProcStateUncachedToCached_screenDimWakeLockEnabled() {
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenDimWakeLock",
+ PowerManager.SCREEN_DIM_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+
+ setCachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureEnabledProcStateCachedToUncached_fullWakeLockEnabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("fullWakeLock", PowerManager.FULL_WAKE_LOCK);
+ setCachedUidProcState(wakeLock.mOwnerUid);
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureDisabledProcStateCachedToUncached_fullWakeLockEnabled() {
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("fullWakeLock", PowerManager.FULL_WAKE_LOCK);
+ setCachedUidProcState(wakeLock.mOwnerUid);
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureEnabledProcStateCachedToUncached_screenBrightWakeLockEnabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenBrightWakeLock",
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK);
+ setCachedUidProcState(wakeLock.mOwnerUid);
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureDisabledProcStateCachedToUncached_screenBrightWakeLockEnabled() {
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenBrightWakeLock",
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK);
+ setCachedUidProcState(wakeLock.mOwnerUid);
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureEnabledProcStateCachedToUncached_screenDimWakeLockEnabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenDimWakeLock",
+ PowerManager.SCREEN_DIM_WAKE_LOCK);
+ setCachedUidProcState(wakeLock.mOwnerUid);
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureDisabledProcStateCachedToUncached_screenDimWakeLockEnabled() {
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ createService();
+ startSystem();
+ WakeLock wakeLock = acquireWakeLock("screenDimWakeLock",
+ PowerManager.SCREEN_DIM_WAKE_LOCK);
+ setCachedUidProcState(wakeLock.mOwnerUid);
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ @Test
+ public void testFeatureDynamicallyDisabledProcStateUncachedToCached_fullWakeLockEnabled() {
+ doReturn(true).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ ArgumentCaptor<DeviceConfig.OnPropertiesChangedListener> listenerCaptor =
+ ArgumentCaptor.forClass(DeviceConfig.OnPropertiesChangedListener.class);
+ createService();
+ startSystem();
+ verify(mDeviceParameterProvider, times(1))
+ .addOnPropertiesChangedListener(any(), listenerCaptor.capture());
+ WakeLock wakeLock = acquireWakeLock("fullWakeLock", PowerManager.FULL_WAKE_LOCK);
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ // dynamically disable the feature
+ doReturn(false).when(mDeviceParameterProvider)
+ .isDisableScreenWakeLocksWhileCachedFeatureEnabled();
+ listenerCaptor.getValue().onPropertiesChanged(
+ new DeviceConfig.Properties("ignored_namespace", null));
+
+ setUncachedUidProcState(wakeLock.mOwnerUid);
+ assertThat(wakeLock.mDisabled).isFalse();
+ }
+
+ private void setCachedUidProcState(int uid) {
+ mService.updateUidProcStateInternal(uid, PROCESS_STATE_TOP_SLEEPING);
+ }
+
+ private void setUncachedUidProcState(int uid) {
+ mService.updateUidProcStateInternal(uid, PROCESS_STATE_RECEIVER);
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 3c882dc871fd..6b225fc945d5 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -73,6 +73,7 @@ import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS;
import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING;
+import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
@@ -80,7 +81,6 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
-import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ALLOW_DISMISS_ONGOING;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.FSI_FORCE_DEMOTE;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.WAKE_LOCK_FOR_POSTING_NOTIFICATION;
@@ -422,6 +422,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Mock
MultiRateLimiter mToastRateLimiter;
BroadcastReceiver mPackageIntentReceiver;
+ BroadcastReceiver mUserSwitchIntentReceiver;
NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
@@ -610,6 +611,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
return null;
}).when(mAppOpsManager).startWatchingMode(eq(AppOpsManager.OP_POST_NOTIFICATION), any(),
any());
+ when(mUmInternal.isUserInitialized(anyInt())).thenReturn(true);
mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
@@ -658,8 +660,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
&& filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
mPackageIntentReceiver = broadcastReceivers.get(i);
}
+ if (filter.hasAction(Intent.ACTION_USER_SWITCHED)) {
+ mUserSwitchIntentReceiver = broadcastReceivers.get(i);
+ }
}
assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
+ assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver);
// Pretend the shortcut exists
List<ShortcutInfo> shortcutInfos = new ArrayList<>();
@@ -699,6 +705,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
mTestFlagResolver.setFlagOverride(FSI_FORCE_DEMOTE, false);
mTestFlagResolver.setFlagOverride(SHOW_STICKY_HUN_FOR_DENIED_FSI, false);
+
+ var checker = mock(TestableNotificationManagerService.ComponentPermissionChecker.class);
+ mService.permissionChecker = checker;
+ when(checker.check(anyString(), anyInt(), anyInt(), anyBoolean()))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
}
@After
@@ -2009,10 +2020,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
final StatusBarNotification sbn = generateNotificationRecord(null).getSbn();
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(),
sbn.getNotification(), sbn.getUserId());
- Thread.sleep(1); // make sure the system clock advances before the next step
+ mTestableLooper.moveTimeForward(1);
// THEN it is canceled
mBinderService.cancelNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getUserId());
- Thread.sleep(1); // here too
+ mTestableLooper.moveTimeForward(1);
// THEN it is posted again (before the cancel has a chance to finish)
mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(),
sbn.getNotification(), sbn.getUserId());
@@ -2303,7 +2314,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
notif.getNotification().flags |= Notification.FLAG_NO_CLEAR;
mService.addNotification(notif);
mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0,
- notif.getUserId(), 0);
+ notif.getUserId(), REASON_CANCEL);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -3041,7 +3052,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
notif.getNotification().flags |= Notification.FLAG_NO_CLEAR;
mService.addNotification(notif);
mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0,
- Notification.FLAG_ONGOING_EVENT, notif.getUserId(), 0);
+ Notification.FLAG_ONGOING_EVENT, notif.getUserId(), REASON_CANCEL);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -3069,7 +3080,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
notif.getNotification().flags |= Notification.FLAG_ONGOING_EVENT;
mService.addNotification(notif);
mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0,
- notif.getUserId(), 0);
+ notif.getUserId(), REASON_CANCEL);
waitForIdle();
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
@@ -3223,6 +3234,150 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void onOpChanged_permissionRevoked_cancelsAllNotificationsFromPackage()
+ throws RemoteException {
+ // Have preexisting posted notifications from revoked package and other packages.
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("revoked", 1001, 1, 0), mTestNotificationChannel));
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
+ // Have preexisting enqueued notifications from revoked package and other packages.
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("revoked", 1001, 3, 0), mTestNotificationChannel));
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+
+ when(mPackageManagerInternal.getPackageUid("revoked", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "revoked", 0);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getSbn().getPackageName()).isEqualTo("other");
+ assertThat(mService.mEnqueuedNotifications).hasSize(1);
+ assertThat(mService.mEnqueuedNotifications.get(0).getSbn().getPackageName()).isEqualTo(
+ "other");
+ }
+
+ @Test
+ public void onOpChanged_permissionStillGranted_notificationsAreNotAffected()
+ throws RemoteException {
+ // NOTE: This combination (receiving the onOpChanged broadcast for a package, the permission
+ // being now granted, AND having previously posted notifications from said package) should
+ // never happen (if we trust the broadcasts are correct). So this test is for a what-if
+ // scenario, to verify we still handle it reasonably.
+
+ // Have preexisting posted notifications from specific package and other packages.
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("granted", 1001, 1, 0), mTestNotificationChannel));
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
+ // Have preexisting enqueued notifications from specific package and other packages.
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("granted", 1001, 3, 0), mTestNotificationChannel));
+ mService.addEnqueuedNotification(new NotificationRecord(mContext,
+ generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+
+ when(mPackageManagerInternal.getPackageUid("granted", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "granted", 0);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(2);
+ assertThat(mService.mEnqueuedNotifications).hasSize(2);
+ }
+
+ @Test
+ public void onOpChanged_notInitializedUser_ignored() throws RemoteException {
+ when(mUmInternal.isUserInitialized(eq(0))).thenReturn(false);
+
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
+ waitForIdle();
+
+ // We early-exited and didn't even query PM for package details.
+ verify(mPackageManagerInternal, never()).getPackageUid(any(), anyLong(), anyInt());
+ }
+
+ @Test
+ public void setNotificationsEnabledForPackage_disabling_clearsNotifications() throws Exception {
+ mService.addNotification(new NotificationRecord(mContext,
+ generateSbn("package", 1001, 1, 0), mTestNotificationChannel));
+ assertThat(mService.mNotificationList).hasSize(1);
+ when(mPackageManagerInternal.getPackageUid("package", 0, 0)).thenReturn(1001);
+ when(mPermissionHelper.hasRequestedPermission(any(), eq("package"), anyInt())).thenReturn(
+ true);
+
+ // Start with granted permission and simulate effect of revoking it.
+ when(mPermissionHelper.hasPermission(1001)).thenReturn(true);
+ doAnswer(invocation -> {
+ when(mPermissionHelper.hasPermission(1001)).thenReturn(false);
+ mOnPermissionChangeListener.onOpChanged(
+ AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
+ return null;
+ }).when(mPermissionHelper).setNotificationPermission("package", 0, false, true);
+
+ mBinderService.setNotificationsEnabledForPackage("package", 1001, false);
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(0);
+
+ mTestableLooper.moveTimeForward(500);
+ waitForIdle();
+ verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null));
+ }
+
+ @Test
+ public void testUpdateAppNotifyCreatorBlock() throws Exception {
+ when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+
+ mBinderService.setNotificationsEnabledForPackage(PKG, mUid, false);
+ Thread.sleep(500);
+ waitForIdle();
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext, times(1)).sendBroadcastAsUser(captor.capture(), any(), eq(null));
+
+ assertEquals(NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED,
+ captor.getValue().getAction());
+ assertEquals(PKG, captor.getValue().getPackage());
+ assertTrue(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, true));
+ }
+
+ @Test
+ public void testUpdateAppNotifyCreatorBlock_notIfMatchesExistingSetting() throws Exception {
+ when(mPermissionHelper.hasPermission(mUid)).thenReturn(false);
+
+ mBinderService.setNotificationsEnabledForPackage(PKG, 0, false);
+ verify(mContext, never()).sendBroadcastAsUser(any(), any(), eq(null));
+ }
+
+ @Test
+ public void testUpdateAppNotifyCreatorUnblock() throws Exception {
+ when(mPermissionHelper.hasPermission(mUid)).thenReturn(false);
+
+ mBinderService.setNotificationsEnabledForPackage(PKG, mUid, true);
+ Thread.sleep(500);
+ waitForIdle();
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mContext, times(1)).sendBroadcastAsUser(captor.capture(), any(), eq(null));
+
+ assertEquals(NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED,
+ captor.getValue().getAction());
+ assertEquals(PKG, captor.getValue().getPackage());
+ assertFalse(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, true));
+ }
+
+ @Test
public void testUpdateChannelNotifyCreatorBlock() throws Exception {
mService.setPreferencesHelper(mPreferencesHelper);
when(mPreferencesHelper.getNotificationChannel(eq(PKG), anyInt(),
@@ -4200,6 +4355,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void testSetNASMigrationDoneAndResetDefault_enableNAS() throws Exception {
int userId = 10;
+ setNASMigrationDone(false, userId);
when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId});
mBinderService.setNASMigrationDoneAndResetDefault(userId, true);
@@ -4211,6 +4367,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void testSetNASMigrationDoneAndResetDefault_disableNAS() throws Exception {
int userId = 10;
+ setNASMigrationDone(false, userId);
when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId});
mBinderService.setNASMigrationDoneAndResetDefault(userId, false);
@@ -4223,6 +4380,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testSetNASMigrationDoneAndResetDefault_multiProfile() throws Exception {
int userId1 = 11;
int userId2 = 12; //work profile
+ setNASMigrationDone(false, userId1);
+ setNASMigrationDone(false, userId2);
setUsers(new int[]{userId1, userId2});
when(mUm.isManagedProfile(userId2)).thenReturn(true);
when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1, userId2});
@@ -4236,6 +4395,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void testSetNASMigrationDoneAndResetDefault_multiUser() throws Exception {
int userId1 = 11;
int userId2 = 12;
+ setNASMigrationDone(false, userId1);
+ setNASMigrationDone(false, userId2);
setUsers(new int[]{userId1, userId2});
when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1});
when(mUm.getProfileIds(userId2, false)).thenReturn(new int[]{userId2});
@@ -11168,8 +11329,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
ai.packageName)).thenReturn(AppOpsManager.MODE_IGNORED);
// Given: a notification from an app on the system partition has the flag
// FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
.build();
@@ -11185,8 +11344,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void fixMediaNotification_withOnGoingFlag_shouldBeNonDismissible()
throws Exception {
// Given: a media notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
.setStyle(new Notification.MediaStyle()
@@ -11215,8 +11372,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
ai.packageName)).thenReturn(AppOpsManager.MODE_IGNORED);
// Given: a notification from an app on the system partition has the flag
// FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
.build();
@@ -11232,9 +11387,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void fixCallNotification_withOnGoingFlag_shouldNotBeNonDismissible()
throws Exception {
// Given: a call notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
-
Person person = new Person.Builder()
.setName("caller")
.build();
@@ -11255,8 +11407,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void fixNonExemptNotification_withOnGoingFlag_shouldBeDismissible() throws Exception {
// Given: a non-exempt notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
.build();
@@ -11273,8 +11423,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
throws Exception {
// Given: a non-exempt notification has the flag FLAG_NO_DISMISS set (even though this is
// not allowed)
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.build();
n.flags |= Notification.FLAG_NO_DISMISS;
@@ -11289,8 +11437,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
@Test
public void fixMediaNotification_withoutOnGoingFlag_shouldBeDismissible() throws Exception {
// Given: a media notification doesn't have the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(false)
.setStyle(new Notification.MediaStyle()
@@ -11309,8 +11455,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
throws Exception {
// Given: a media notification doesn't have the flag FLAG_ONGOING_EVENT set,
// but has the flag FLAG_NO_DISMISS set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(false)
.setStyle(new Notification.MediaStyle()
@@ -11329,8 +11473,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
public void fixNonExempt_Notification_withoutOnGoingFlag_shouldBeDismissible()
throws Exception {
// Given: a non-exempt notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(false)
.build();
@@ -11347,8 +11489,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
throws Exception {
when(mDevicePolicyManager.isActiveDeviceOwner(mUid)).thenReturn(true);
// Given: a notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
setDpmAppOppsExemptFromDismissal(false);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
@@ -11375,8 +11515,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, mUid,
PKG)).thenReturn(AppOpsManager.MODE_ALLOWED);
// Given: a notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
setDpmAppOppsExemptFromDismissal(true);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
@@ -11396,8 +11534,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, mUid,
PKG)).thenReturn(AppOpsManager.MODE_ALLOWED);
// Given: a notification has the flag FLAG_ONGOING_EVENT set
- // feature flag: ALLOW_DISMISS_ONGOING is on
- mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
setDpmAppOppsExemptFromDismissal(false);
Notification n = new Notification.Builder(mContext, "test")
.setOngoing(true)
@@ -12139,131 +12275,142 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
- public void onOpChanged_permissionRevoked_cancelsAllNotificationsFromPackage()
- throws RemoteException {
- // Have preexisting posted notifications from revoked package and other packages.
- mService.addNotification(new NotificationRecord(mContext,
- generateSbn("revoked", 1001, 1, 0), mTestNotificationChannel));
- mService.addNotification(new NotificationRecord(mContext,
- generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
- // Have preexisting enqueued notifications from revoked package and other packages.
- mService.addEnqueuedNotification(new NotificationRecord(mContext,
- generateSbn("revoked", 1001, 3, 0), mTestNotificationChannel));
- mService.addEnqueuedNotification(new NotificationRecord(mContext,
- generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
- assertThat(mService.mNotificationList).hasSize(2);
- assertThat(mService.mEnqueuedNotifications).hasSize(2);
+ public void onUserSwitched_updatesZenModeAndChannelsBypassingDnd() {
+ Intent intent = new Intent(Intent.ACTION_USER_SWITCHED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, 20);
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ mService.setPreferencesHelper(mPreferencesHelper);
- when(mPackageManagerInternal.getPackageUid("revoked", 0, 0)).thenReturn(1001);
- when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false);
+ mUserSwitchIntentReceiver.onReceive(mContext, intent);
- mOnPermissionChangeListener.onOpChanged(
- AppOpsManager.OPSTR_POST_NOTIFICATION, "revoked", 0);
- waitForIdle();
+ InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
+ inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
+ inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+ inOrder.verifyNoMoreInteractions();
+ }
- assertThat(mService.mNotificationList).hasSize(1);
- assertThat(mService.mNotificationList.get(0).getSbn().getPackageName()).isEqualTo("other");
- assertThat(mService.mEnqueuedNotifications).hasSize(1);
- assertThat(mService.mEnqueuedNotifications.get(0).getSbn().getPackageName()).isEqualTo(
- "other");
+ @Test
+ public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception {
+ final String notReal = "NOT REAL";
+ final var checker = mService.permissionChecker;
+
+ when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow(
+ PackageManager.NameNotFoundException.class);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt());
+ verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean());
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
}
@Test
- public void onOpChanged_permissionStillGranted_notificationsAreNotAffected()
- throws RemoteException {
- // NOTE: This combination (receiving the onOpChanged broadcast for a package, the permission
- // being now granted, AND having previously posted notifications from said package) should
- // never happen (if we trust the broadcasts are correct). So this test is for a what-if
- // scenario, to verify we still handle it reasonably.
+ public void isNotificationPolicyAccessGranted_hasPermission() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
- // Have preexisting posted notifications from specific package and other packages.
- mService.addNotification(new NotificationRecord(mContext,
- generateSbn("granted", 1001, 1, 0), mTestNotificationChannel));
- mService.addNotification(new NotificationRecord(mContext,
- generateSbn("other", 1002, 2, 0), mTestNotificationChannel));
- // Have preexisting enqueued notifications from specific package and other packages.
- mService.addEnqueuedNotification(new NotificationRecord(mContext,
- generateSbn("granted", 1001, 3, 0), mTestNotificationChannel));
- mService.addEnqueuedNotification(new NotificationRecord(mContext,
- generateSbn("other", 1002, 4, 0), mTestNotificationChannel));
- assertThat(mService.mNotificationList).hasSize(2);
- assertThat(mService.mEnqueuedNotifications).hasSize(2);
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
- when(mPackageManagerInternal.getPackageUid("granted", 0, 0)).thenReturn(1001);
- when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true);
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
+ }
- mOnPermissionChangeListener.onOpChanged(
- AppOpsManager.OPSTR_POST_NOTIFICATION, "granted", 0);
- waitForIdle();
+ @Test
+ public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
- assertThat(mService.mNotificationList).hasSize(2);
- assertThat(mService.mEnqueuedNotifications).hasSize(2);
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt()))
+ .thenReturn(true);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners, never()).isComponentEnabledForPackage(any());
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
}
@Test
- public void onOpChanged_permissionGranted_notifiesAppUnblocked() throws Exception {
- when(mPackageManagerInternal.getPackageUid(PKG, 0, 0)).thenReturn(1001);
- when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true);
+ public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
- mOnPermissionChangeListener.onOpChanged(
- AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0);
- waitForIdle();
- Thread.sleep(600);
- waitForIdle();
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mListeners.isComponentEnabledForPackage(packageName)).thenReturn(true);
- ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
- verify(mContext).sendBroadcastAsUser(captor.capture(), any(), eq(null));
- assertThat(captor.getValue().getAction()).isEqualTo(
- NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED);
- assertThat(captor.getValue().getPackage()).isEqualTo(PKG);
- assertThat(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, true)).isFalse();
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt());
}
@Test
- public void onOpChanged_permissionRevoked_notifiesAppBlocked() throws Exception {
- when(mPackageManagerInternal.getPackageUid(PKG, 0, 0)).thenReturn(1001);
- when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false);
+ public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
- mOnPermissionChangeListener.onOpChanged(
- AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0);
- waitForIdle();
- Thread.sleep(600);
- waitForIdle();
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true);
- ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
- verify(mContext).sendBroadcastAsUser(captor.capture(), any(), eq(null));
- assertThat(captor.getValue().getAction()).isEqualTo(
- NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED);
- assertThat(captor.getValue().getPackage()).isEqualTo(PKG);
- assertThat(captor.getValue().getBooleanExtra(EXTRA_BLOCKED_STATE, false)).isTrue();
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
}
+ /**
+ * b/292163859
+ */
@Test
- public void setNotificationsEnabledForPackage_disabling_clearsNotifications() throws Exception {
- mService.addNotification(new NotificationRecord(mContext,
- generateSbn("package", 1001, 1, 0), mTestNotificationChannel));
- assertThat(mService.mNotificationList).hasSize(1);
- when(mPackageManagerInternal.getPackageUid("package", 0, 0)).thenReturn(1001);
- when(mPermissionHelper.hasRequestedPermission(any(), eq("package"), anyInt())).thenReturn(
- true);
+ public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final int callingUid = Binder.getCallingUid();
+ final var checker = mService.permissionChecker;
- // Start with granted permission and simulate effect of revoking it.
- when(mPermissionHelper.hasPermission(1001)).thenReturn(true);
- doAnswer(invocation -> {
- when(mPermissionHelper.hasPermission(1001)).thenReturn(false);
- mOnPermissionChangeListener.onOpChanged(
- AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0);
- return null;
- }).when(mPermissionHelper).setNotificationPermission("package", 0, false, true);
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+ when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true);
- mBinderService.setNotificationsEnabledForPackage("package", 1001, false);
- waitForIdle();
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
+ verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid);
+ }
- assertThat(mService.mNotificationList).hasSize(0);
+ @Test
+ public void isNotificationPolicyAccessGranted_notGranted() throws Exception {
+ final String packageName = "target";
+ final int uid = 123;
+ final var checker = mService.permissionChecker;
- Thread.sleep(600);
- waitForIdle();
- verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null));
+ when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid);
+
+ assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse();
+ verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt());
+ verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true);
+ verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt());
+ verify(mListeners).isComponentEnabledForPackage(packageName);
+ verify(mDevicePolicyManager).isActiveDeviceOwner(uid);
}
private static <T extends Parcelable> T parcelAndUnparcel(T source,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index ea670bd94d88..c242554e950b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -2478,7 +2478,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
-
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2509,6 +2509,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2533,6 +2534,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2587,6 +2589,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
assertFalse(mHelper.areChannelsBypassingDnd());
verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean());
resetZenModeHelper();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 9f4eee7e332f..27e8f3664a65 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -43,6 +43,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi
@Nullable
Boolean mIsVisibleToListenerReturnValue = null;
+ ComponentPermissionChecker permissionChecker;
+
TestableNotificationManagerService(Context context, NotificationRecordLogger logger,
InstanceIdSequence notificationInstanceIdSequence) {
super(context, logger, notificationInstanceIdSequence);
@@ -150,6 +152,12 @@ public class TestableNotificationManagerService extends NotificationManagerServi
return super.isVisibleToListener(sbn, notificationType, listener);
}
+ @Override
+ protected int checkComponentPermission(String permission, int uid, int owningUid,
+ boolean exported) {
+ return permissionChecker.check(permission, uid, owningUid, exported);
+ }
+
public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker {
private int mGetStrongAuthForUserReturnValue = 0;
StrongAuthTrackerFake(Context context) {
@@ -165,4 +173,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi
return mGetStrongAuthForUserReturnValue;
}
}
+
+ public interface ComponentPermissionChecker {
+ int check(String permission, int uid, int owningUid, boolean exported);
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index dedb8f170ee0..3ee75de23fdb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -771,7 +771,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
mZenModeHelper.mConfig = null; // will evaluate config to zen mode off
for (int i = 0; i < 3; i++) {
// if zen doesn't change, zen should not reapply itself to the ringer
- mZenModeHelper.evaluateZenMode("test", true);
+ mZenModeHelper.evaluateZenModeLocked("test", true);
}
verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
mZenModeHelper.TAG);
@@ -798,7 +798,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
for (int i = 0; i < 3; i++) {
// if zen doesn't change, zen should not reapply itself to the ringer
- mZenModeHelper.evaluateZenMode("test", true);
+ mZenModeHelper.evaluateZenModeLocked("test", true);
}
verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
mZenModeHelper.TAG);
@@ -825,7 +825,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
for (int i = 0; i < 3; i++) {
// if zen doesn't change, zen should not reapply itself to the ringer
- mZenModeHelper.evaluateZenMode("test", true);
+ mZenModeHelper.evaluateZenModeLocked("test", true);
}
verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
mZenModeHelper.TAG);
@@ -2269,7 +2269,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// Artificially turn zen mode "on". Re-evaluating zen mode should cause it to turn back off
// given that we don't have any zen rules active.
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.evaluateZenMode("test", true);
+ mZenModeHelper.evaluateZenModeLocked("test", true);
// Check that the change actually took: zen mode should be off now
assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp
index 5f48f3ce1d68..6aa59895fb82 100644
--- a/services/tests/wmtests/Android.bp
+++ b/services/tests/wmtests/Android.bp
@@ -51,6 +51,7 @@ android_test {
"services.core",
"androidx.test.runner",
"androidx.test.rules",
+ "junit-params",
"mockito-target-extended-minus-junit4",
"platform-test-annotations",
"servicestests-utils",
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 2015ae9b8081..bf88ce460b99 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -63,7 +63,7 @@ class ShortcutKeyTestBase {
final Context mContext = spy(getInstrumentation().getTargetContext());
/** Modifier key to meta state */
- private static final Map<Integer, Integer> MODIFIER;
+ protected static final Map<Integer, Integer> MODIFIER;
static {
final Map<Integer, Integer> map = new ArrayMap<>();
map.put(KEYCODE_CTRL_LEFT, META_CTRL_LEFT_ON | META_CTRL_ON);
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
new file mode 100644
index 000000000000..feca3268a95c
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import android.platform.test.annotations.Presubmit;
+import android.view.KeyEvent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.annotations.Keep;
+import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+@Presubmit
+@SmallTest
+@RunWith(JUnitParamsRunner.class)
+public class ShortcutLoggingTests extends ShortcutKeyTestBase {
+
+ private static final int VENDOR_ID = 0x123;
+ private static final int PRODUCT_ID = 0x456;
+ private static final int META_KEY = KeyEvent.KEYCODE_META_LEFT;
+ private static final int META_ON = MODIFIER.get(KeyEvent.KEYCODE_META_LEFT);
+ private static final int ALT_KEY = KeyEvent.KEYCODE_ALT_LEFT;
+ private static final int ALT_ON = MODIFIER.get(KeyEvent.KEYCODE_ALT_LEFT);
+ private static final int CTRL_KEY = KeyEvent.KEYCODE_CTRL_LEFT;
+ private static final int CTRL_ON = MODIFIER.get(KeyEvent.KEYCODE_CTRL_LEFT);
+ private static final int SHIFT_KEY = KeyEvent.KEYCODE_SHIFT_LEFT;
+ private static final int SHIFT_ON = MODIFIER.get(KeyEvent.KEYCODE_SHIFT_LEFT);
+
+ @Keep
+ private static Object[][] shortcutTestArguments() {
+ // testName, testKeys, expectedLogEvent, expectedKey, expectedModifierState
+ return new Object[][]{
+ {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
+ KeyboardLogEvent.HOME, KeyEvent.KEYCODE_H, META_ON},
+ {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
+ KeyboardLogEvent.HOME, KeyEvent.KEYCODE_ENTER, META_ON},
+ {"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME}, KeyboardLogEvent.HOME,
+ KeyEvent.KEYCODE_HOME, 0},
+ {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
+ KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_RECENT_APPS, 0},
+ {"Meta + Tab -> Open OVerview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
+ KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_TAB, META_ON},
+ {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
+ KeyboardLogEvent.RECENT_APPS, KeyEvent.KEYCODE_TAB, ALT_ON},
+ {"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK}, KeyboardLogEvent.BACK,
+ KeyEvent.KEYCODE_BACK, 0},
+ {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
+ KeyboardLogEvent.APP_SWITCH, KeyEvent.KEYCODE_APP_SWITCH, 0},
+ {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
+ KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_ASSIST, 0},
+ {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
+ KeyboardLogEvent.LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A, META_ON},
+ {"VOICE_ASSIST key -> Launch Voice Assistant",
+ new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
+ KeyboardLogEvent.LAUNCH_VOICE_ASSISTANT, KeyEvent.KEYCODE_VOICE_ASSIST, 0},
+ {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
+ KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS, KeyEvent.KEYCODE_I, META_ON},
+ {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
+ KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_N, META_ON},
+ {"NOTIFICATION key -> Toggle Notification Panel",
+ new int[]{KeyEvent.KEYCODE_NOTIFICATION},
+ KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION,
+ 0},
+ {"Meta + T -> Toggle Taskbar", new int[]{META_KEY, KeyEvent.KEYCODE_T},
+ KeyboardLogEvent.TOGGLE_TASKBAR, KeyEvent.KEYCODE_T, META_ON},
+ {"Meta + Ctrl + S -> Take Screenshot",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
+ KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON},
+ {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
+ KeyboardLogEvent.OPEN_SHORTCUT_HELPER, KeyEvent.KEYCODE_SLASH, META_ON},
+ {"BRIGHTNESS_UP key -> Increase Brightness",
+ new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP}, KeyboardLogEvent.BRIGHTNESS_UP,
+ KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
+ {"BRIGHTNESS_DOWN key -> Decrease Brightness",
+ new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
+ KeyboardLogEvent.BRIGHTNESS_DOWN, KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
+ {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
+ KeyboardLogEvent.KEYBOARD_BACKLIGHT_UP,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
+ {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
+ KeyboardLogEvent.KEYBOARD_BACKLIGHT_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
+ {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
+ KeyboardLogEvent.KEYBOARD_BACKLIGHT_TOGGLE,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
+ {"VOLUME_UP key -> Increase Volume", new int[]{KeyEvent.KEYCODE_VOLUME_UP},
+ KeyboardLogEvent.VOLUME_UP, KeyEvent.KEYCODE_VOLUME_UP, 0},
+ {"VOLUME_DOWN key -> Decrease Volume", new int[]{KeyEvent.KEYCODE_VOLUME_DOWN},
+ KeyboardLogEvent.VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN, 0},
+ {"VOLUME_MUTE key -> Mute Volume", new int[]{KeyEvent.KEYCODE_VOLUME_MUTE},
+ KeyboardLogEvent.VOLUME_MUTE, KeyEvent.KEYCODE_VOLUME_MUTE, 0},
+ {"ALL_APPS key -> Open App Drawer", new int[]{KeyEvent.KEYCODE_ALL_APPS},
+ KeyboardLogEvent.ALL_APPS, KeyEvent.KEYCODE_ALL_APPS, 0},
+ {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
+ KeyboardLogEvent.LAUNCH_SEARCH, KeyEvent.KEYCODE_SEARCH, 0},
+ {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
+ new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
+ KeyboardLogEvent.LANGUAGE_SWITCH, KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
+ {"Meta + Space -> Switch Keyboard Language",
+ new int[]{META_KEY, KeyEvent.KEYCODE_SPACE},
+ KeyboardLogEvent.LANGUAGE_SWITCH, KeyEvent.KEYCODE_SPACE, META_ON},
+ {"Meta + Shift + Space -> Switch Keyboard Language",
+ new int[]{META_KEY, SHIFT_KEY, KeyEvent.KEYCODE_SPACE},
+ KeyboardLogEvent.LANGUAGE_SWITCH, KeyEvent.KEYCODE_SPACE,
+ META_ON | SHIFT_ON},
+ {"META key -> Open App Drawer in Accessibility mode", new int[]{META_KEY},
+ KeyboardLogEvent.ACCESSIBILITY_ALL_APPS, META_KEY, META_ON},
+ {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
+ KeyboardLogEvent.TOGGLE_CAPS_LOCK, ALT_KEY, META_ON | ALT_ON},
+ {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
+ KeyboardLogEvent.TOGGLE_CAPS_LOCK, META_KEY, META_ON | ALT_ON},
+ {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
+ KeyboardLogEvent.TOGGLE_CAPS_LOCK, KeyEvent.KEYCODE_CAPS_LOCK, 0},
+ {"MUTE key -> Mute System Microphone", new int[]{KeyEvent.KEYCODE_MUTE},
+ KeyboardLogEvent.SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE, 0},
+ {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
+ KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION, KeyEvent.KEYCODE_DPAD_UP,
+ META_ON | CTRL_ON},
+ {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+ KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION, KeyEvent.KEYCODE_DPAD_LEFT,
+ META_ON | CTRL_ON},
+ {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
+ KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION, KeyEvent.KEYCODE_DPAD_RIGHT,
+ META_ON | CTRL_ON},
+ {"Shift + Menu -> Trigger Bug Report", new int[]{SHIFT_KEY, KeyEvent.KEYCODE_MENU},
+ KeyboardLogEvent.TRIGGER_BUG_REPORT, KeyEvent.KEYCODE_MENU, SHIFT_ON},
+ {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
+ KeyboardLogEvent.LOCK_SCREEN, KeyEvent.KEYCODE_L, META_ON},
+ {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
+ KeyboardLogEvent.OPEN_NOTES, KeyEvent.KEYCODE_N, META_ON | CTRL_ON},
+ {"POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_POWER},
+ KeyboardLogEvent.TOGGLE_POWER, KeyEvent.KEYCODE_POWER, 0},
+ {"TV_POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_TV_POWER},
+ KeyboardLogEvent.TOGGLE_POWER, KeyEvent.KEYCODE_TV_POWER, 0},
+ {"SYSTEM_NAVIGATION_DOWN key -> System Navigation",
+ new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN},
+ KeyboardLogEvent.SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
+ 0},
+ {"SYSTEM_NAVIGATION_UP key -> System Navigation",
+ new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP},
+ KeyboardLogEvent.SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
+ 0},
+ {"SYSTEM_NAVIGATION_LEFT key -> System Navigation",
+ new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT},
+ KeyboardLogEvent.SYSTEM_NAVIGATION, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT,
+ 0},
+ {"SYSTEM_NAVIGATION_RIGHT key -> System Navigation",
+ new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT},
+ KeyboardLogEvent.SYSTEM_NAVIGATION,
+ KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, 0},
+ {"SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SLEEP},
+ KeyboardLogEvent.SLEEP, KeyEvent.KEYCODE_SLEEP, 0},
+ {"SOFT_SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SOFT_SLEEP},
+ KeyboardLogEvent.SLEEP, KeyEvent.KEYCODE_SOFT_SLEEP, 0},
+ {"WAKEUP key -> System Wakeup", new int[]{KeyEvent.KEYCODE_WAKEUP},
+ KeyboardLogEvent.WAKEUP, KeyEvent.KEYCODE_WAKEUP, 0},
+ {"MEDIA_PLAY key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PLAY},
+ KeyboardLogEvent.MEDIA_KEY, KeyEvent.KEYCODE_MEDIA_PLAY, 0},
+ {"MEDIA_PAUSE key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PAUSE},
+ KeyboardLogEvent.MEDIA_KEY, KeyEvent.KEYCODE_MEDIA_PAUSE, 0},
+ {"MEDIA_PLAY_PAUSE key -> Media Control",
+ new int[]{KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}, KeyboardLogEvent.MEDIA_KEY,
+ KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, 0},
+ {"Meta + B -> Launch Default Browser", new int[]{META_KEY, KeyEvent.KEYCODE_B},
+ KeyboardLogEvent.LAUNCH_DEFAULT_BROWSER, KeyEvent.KEYCODE_B, META_ON},
+ {"EXPLORER key -> Launch Default Browser", new int[]{KeyEvent.KEYCODE_EXPLORER},
+ KeyboardLogEvent.LAUNCH_DEFAULT_BROWSER, KeyEvent.KEYCODE_EXPLORER, 0},
+ {"Meta + C -> Launch Default Contacts", new int[]{META_KEY, KeyEvent.KEYCODE_C},
+ KeyboardLogEvent.LAUNCH_DEFAULT_CONTACTS, KeyEvent.KEYCODE_C, META_ON},
+ {"CONTACTS key -> Launch Default Contacts", new int[]{KeyEvent.KEYCODE_CONTACTS},
+ KeyboardLogEvent.LAUNCH_DEFAULT_CONTACTS, KeyEvent.KEYCODE_CONTACTS, 0},
+ {"Meta + E -> Launch Default Email", new int[]{META_KEY, KeyEvent.KEYCODE_E},
+ KeyboardLogEvent.LAUNCH_DEFAULT_EMAIL, KeyEvent.KEYCODE_E, META_ON},
+ {"ENVELOPE key -> Launch Default Email", new int[]{KeyEvent.KEYCODE_ENVELOPE},
+ KeyboardLogEvent.LAUNCH_DEFAULT_EMAIL, KeyEvent.KEYCODE_ENVELOPE, 0},
+ {"Meta + K -> Launch Default Calendar", new int[]{META_KEY, KeyEvent.KEYCODE_K},
+ KeyboardLogEvent.LAUNCH_DEFAULT_CALENDAR, KeyEvent.KEYCODE_K, META_ON},
+ {"CALENDAR key -> Launch Default Calendar", new int[]{KeyEvent.KEYCODE_CALENDAR},
+ KeyboardLogEvent.LAUNCH_DEFAULT_CALENDAR, KeyEvent.KEYCODE_CALENDAR, 0},
+ {"Meta + P -> Launch Default Music", new int[]{META_KEY, KeyEvent.KEYCODE_P},
+ KeyboardLogEvent.LAUNCH_DEFAULT_MUSIC, KeyEvent.KEYCODE_P, META_ON},
+ {"MUSIC key -> Launch Default Music", new int[]{KeyEvent.KEYCODE_MUSIC},
+ KeyboardLogEvent.LAUNCH_DEFAULT_MUSIC, KeyEvent.KEYCODE_MUSIC, 0},
+ {"Meta + U -> Launch Default Calculator", new int[]{META_KEY, KeyEvent.KEYCODE_U},
+ KeyboardLogEvent.LAUNCH_DEFAULT_CALCULATOR, KeyEvent.KEYCODE_U, META_ON},
+ {"CALCULATOR key -> Launch Default Calculator",
+ new int[]{KeyEvent.KEYCODE_CALCULATOR},
+ KeyboardLogEvent.LAUNCH_DEFAULT_CALCULATOR, KeyEvent.KEYCODE_CALCULATOR, 0},
+ {"Meta + M -> Launch Default Maps", new int[]{META_KEY, KeyEvent.KEYCODE_M},
+ KeyboardLogEvent.LAUNCH_DEFAULT_MAPS, KeyEvent.KEYCODE_M, META_ON},
+ {"Meta + S -> Launch Default Messaging App",
+ new int[]{META_KEY, KeyEvent.KEYCODE_S},
+ KeyboardLogEvent.LAUNCH_DEFAULT_MESSAGING, KeyEvent.KEYCODE_S, META_ON}};
+ }
+
+ @Before
+ @Override
+ public void setUp() {
+ super.setUp();
+ mPhoneWindowManager.overrideKeyEventSource(VENDOR_ID, PRODUCT_ID);
+ mPhoneWindowManager.overrideLaunchHome();
+ mPhoneWindowManager.overrideSearchKeyBehavior(
+ PhoneWindowManager.SEARCH_BEHAVIOR_TARGET_ACTIVITY);
+ mPhoneWindowManager.overrideEnableBugReportTrigger(true);
+ mPhoneWindowManager.overrideStatusBarManagerInternal();
+ mPhoneWindowManager.overrideStartActivity();
+ mPhoneWindowManager.overrideUserSetupComplete();
+ }
+
+ @Test
+ @Parameters(method = "shortcutTestArguments")
+ public void testShortcuts(String testName, int[] testKeys, KeyboardLogEvent expectedLogEvent,
+ int expectedKey, int expectedModifierState) {
+ sendKeyCombination(testKeys, 0);
+ mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, expectedLogEvent,
+ expectedKey, expectedModifierState, "Failed while executing " + testName);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 766a88f6476c..186676772a6a 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -26,6 +26,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.description;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
@@ -35,6 +36,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GO_TO_VOICE_ASSIST;
@@ -50,7 +52,6 @@ import static org.mockito.Mockito.after;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.withSettings;
import android.app.ActivityManagerInternal;
@@ -60,8 +61,10 @@ import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.hardware.SensorPrivacyManager;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
+import android.hardware.input.InputManager;
import android.media.AudioManagerInternal;
import android.os.Handler;
import android.os.HandlerThread;
@@ -75,14 +78,17 @@ import android.service.dreams.DreamManagerInternal;
import android.telecom.TelecomManager;
import android.util.FeatureFlagUtils;
import android.view.Display;
+import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.autofill.AutofillManagerInternal;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.accessibility.AccessibilityShortcutController;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.server.GestureLauncherService;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
+import com.android.server.input.KeyboardMetricsCollector.KeyboardLogEvent;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.pm.UserManagerInternal;
import com.android.server.policy.keyguard.KeyguardServiceDelegate;
@@ -102,6 +108,7 @@ import org.mockito.Mock;
import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
import java.util.function.Supplier;
@@ -117,6 +124,8 @@ class TestPhoneWindowManager {
@Mock private ActivityManagerInternal mActivityManagerInternal;
@Mock private ActivityTaskManagerInternal mActivityTaskManagerInternal;
@Mock private InputManagerInternal mInputManagerInternal;
+ @Mock private InputManager mInputManager;
+ @Mock private SensorPrivacyManager mSensorPrivacyManager;
@Mock private DreamManagerInternal mDreamManagerInternal;
@Mock private PowerManagerInternal mPowerManagerInternal;
@Mock private DisplayManagerInternal mDisplayManagerInternal;
@@ -186,6 +195,8 @@ class TestPhoneWindowManager {
// Return mocked services: LocalServices.getService
mMockitoSession = mockitoSession()
.mockStatic(LocalServices.class, spyStubOnly)
+ .mockStatic(FrameworkStatsLog.class)
+ .strictness(Strictness.LENIENT)
.startMocking();
doReturn(mWindowManagerInternal).when(
@@ -213,7 +224,10 @@ class TestPhoneWindowManager {
doReturn(mAppOpsManager).when(mContext).getSystemService(eq(AppOpsManager.class));
doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
+ doReturn(mInputManager).when(mContext).getSystemService(eq(InputManager.class));
doReturn(mPackageManager).when(mContext).getPackageManager();
+ doReturn(mSensorPrivacyManager).when(mContext).getSystemService(
+ eq(SensorPrivacyManager.class));
doReturn(false).when(mPackageManager).hasSystemFeature(any());
try {
doThrow(new PackageManager.NameNotFoundException("test")).when(mPackageManager)
@@ -409,6 +423,31 @@ class TestPhoneWindowManager {
doReturn(isShowing).when(mKeyguardServiceDelegate).isShowing();
}
+ void overrideKeyEventSource(int vendorId, int productId) {
+ InputDevice device = new InputDevice.Builder().setId(1).setVendorId(vendorId).setProductId(
+ productId).setSources(InputDevice.SOURCE_KEYBOARD).setKeyboardType(
+ InputDevice.KEYBOARD_TYPE_ALPHABETIC).build();
+ doReturn(mInputManager).when(mContext).getSystemService(eq(InputManager.class));
+ doReturn(device).when(mInputManager).getInputDevice(anyInt());
+ }
+
+ void overrideSearchKeyBehavior(int behavior) {
+ mPhoneWindowManager.mSearchKeyBehavior = behavior;
+ }
+
+ void overrideEnableBugReportTrigger(boolean enable) {
+ mPhoneWindowManager.mEnableShiftMenuBugReports = enable;
+ }
+
+ void overrideStartActivity() {
+ doNothing().when(mContext).startActivityAsUser(any(), any());
+ doNothing().when(mContext).startActivityAsUser(any(), any(), any());
+ }
+
+ void overrideUserSetupComplete() {
+ doReturn(true).when(mPhoneWindowManager).isUserSetupComplete();
+ }
+
/**
* Below functions will check the policy behavior could be invoked.
*/
@@ -563,4 +602,12 @@ class TestPhoneWindowManager {
verify(mContext, after(TEST_SINGLE_KEY_DELAY_MILLIS).never())
.startActivityAsUser(any(Intent.class), any(), any(UserHandle.class));
}
+
+ void assertShortcutLogged(int vendorId, int productId, KeyboardLogEvent logEvent,
+ int expectedKey, int expectedModifierState, String errorMsg) {
+ waitForIdle();
+ verify(() -> FrameworkStatsLog.write(FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED,
+ vendorId, productId, logEvent.getIntValue(), new int[]{expectedKey},
+ expectedModifierState), description(errorMsg));
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
index 5c3102d870d0..65e77dcd4ca9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityMetricsLaunchObserverTests.java
@@ -182,12 +182,12 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase {
@Test
public void testLaunchState() {
- final ToIntFunction<Boolean> launchTemplate = doRelaunch -> {
+ final ToIntFunction<Runnable> launchTemplate = action -> {
clearInvocations(mLaunchObserver);
onActivityLaunched(mTopActivity);
notifyTransitionStarting(mTopActivity);
- if (doRelaunch) {
- mActivityMetricsLogger.notifyActivityRelaunched(mTopActivity);
+ if (action != null) {
+ action.run();
}
final ActivityMetricsLogger.TransitionInfoSnapshot info =
notifyWindowsDrawn(mTopActivity);
@@ -199,21 +199,27 @@ public class ActivityMetricsLaunchObserverTests extends WindowTestsBase {
// Assume that the process is started (ActivityBuilder has mocked the returned value of
// ATMS#getProcessController) but the activity has not attached process.
mTopActivity.app = null;
- assertWithMessage("Warm launch").that(launchTemplate.applyAsInt(false /* doRelaunch */))
+ assertWithMessage("Warm launch").that(launchTemplate.applyAsInt(null))
.isEqualTo(WaitResult.LAUNCH_STATE_WARM);
mTopActivity.app = app;
mNewActivityCreated = false;
- assertWithMessage("Hot launch").that(launchTemplate.applyAsInt(false /* doRelaunch */))
+ assertWithMessage("Hot launch").that(launchTemplate.applyAsInt(null))
.isEqualTo(WaitResult.LAUNCH_STATE_HOT);
- assertWithMessage("Relaunch").that(launchTemplate.applyAsInt(true /* doRelaunch */))
+ assertWithMessage("Relaunch").that(launchTemplate.applyAsInt(
+ () -> mActivityMetricsLogger.notifyActivityRelaunched(mTopActivity)))
.isEqualTo(WaitResult.LAUNCH_STATE_RELAUNCH);
+ assertWithMessage("Cold launch by restart").that(launchTemplate.applyAsInt(
+ () -> mActivityMetricsLogger.notifyBindApplication(
+ mTopActivity.info.applicationInfo)))
+ .isEqualTo(WaitResult.LAUNCH_STATE_COLD);
+
mTopActivity.app = null;
mNewActivityCreated = true;
doReturn(null).when(mAtm).getProcessController(app.mName, app.mUid);
- assertWithMessage("Cold launch").that(launchTemplate.applyAsInt(false /* doRelaunch */))
+ assertWithMessage("Cold launch").that(launchTemplate.applyAsInt(null))
.isEqualTo(WaitResult.LAUNCH_STATE_COLD);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index f235d153c658..233a2076a867 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -52,7 +52,8 @@ public class DimmerTests extends WindowTestsBase {
private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
final SurfaceControl mControl = mock(SurfaceControl.class);
- final SurfaceControl.Transaction mTransaction = spy(StubTransaction.class);
+ final SurfaceControl.Transaction mPendingTransaction = spy(StubTransaction.class);
+ final SurfaceControl.Transaction mSyncTransaction = spy(StubTransaction.class);
TestWindowContainer(WindowManagerService wm) {
super(wm);
@@ -65,12 +66,12 @@ public class DimmerTests extends WindowTestsBase {
@Override
public SurfaceControl.Transaction getSyncTransaction() {
- return mTransaction;
+ return mSyncTransaction;
}
@Override
public SurfaceControl.Transaction getPendingTransaction() {
- return mTransaction;
+ return mPendingTransaction;
}
}
@@ -144,7 +145,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
int width = 100;
int height = 300;
@@ -161,13 +162,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(dimLayer, alpha);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1);
+ verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, 1);
}
@Test
@@ -176,13 +177,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimBelow(mTransaction, child, alpha, 0);
+ mDimmer.dimBelow(child, alpha, 0);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setAlpha(dimLayer, alpha);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1);
}
@Test
@@ -191,7 +192,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
@@ -208,10 +209,10 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
SurfaceControl dimLayer = getDimLayer();
mDimmer.resetDimStates();
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
mDimmer.updateDims(mTransaction);
verify(mTransaction).show(dimLayer);
@@ -224,7 +225,7 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final float alpha = 0.8f;
- mDimmer.dimAbove(mTransaction, child, alpha);
+ mDimmer.dimAbove(child, alpha);
final Rect bounds = mDimmer.mDimState.mDimBounds;
SurfaceControl dimLayer = getDimLayer();
@@ -245,7 +246,7 @@ public class DimmerTests extends WindowTestsBase {
TestWindowContainer child = new TestWindowContainer(mWm);
mHost.addChild(child, 0);
- mDimmer.dimAbove(mTransaction, child, 1);
+ mDimmer.dimAbove(child, 1);
SurfaceControl dimLayer = getDimLayer();
mDimmer.updateDims(mTransaction);
verify(mTransaction, times(1)).show(dimLayer);
@@ -266,13 +267,13 @@ public class DimmerTests extends WindowTestsBase {
mHost.addChild(child, 0);
final int blurRadius = 50;
- mDimmer.dimBelow(mTransaction, child, 0, blurRadius);
+ mDimmer.dimBelow(child, 0, blurRadius);
SurfaceControl dimLayer = getDimLayer();
assertNotNull("Dimmer should have created a surface", dimLayer);
- verify(mTransaction).setBackgroundBlurRadius(dimLayer, blurRadius);
- verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1);
+ verify(mHost.getPendingTransaction()).setBackgroundBlurRadius(dimLayer, blurRadius);
+ verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1);
}
private SurfaceControl getDimLayer() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
index 3934b023b9fd..4034dbc70236 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java
@@ -62,28 +62,31 @@ public class InsetsSourceProviderTest extends WindowTestsBase {
@Test
public void testPostLayout() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
+ statusBar.setBounds(0, 0, 500, 1000);
statusBar.getFrame().set(0, 0, 500, 100);
statusBar.mHasSurface = true;
mProvider.setWindowContainer(statusBar, null, null);
mProvider.updateSourceFrame(statusBar.getFrame());
mProvider.onPostLayout();
assertEquals(new Rect(0, 0, 500, 100), mProvider.getSource().getFrame());
- assertEquals(Insets.of(0, 100, 0, 0),
- mProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500),
- false /* ignoreVisibility */));
- assertEquals(Insets.of(0, 100, 0, 0),
- mProvider.getSource().calculateVisibleInsets(new Rect(0, 0, 500, 500)));
+ assertEquals(Insets.of(0, 100, 0, 0), mProvider.getInsetsHint());
+
+ // Change the bounds and call onPostLayout. Make sure the insets hint gets updated.
+ statusBar.setBounds(0, 10, 500, 1000);
+ mProvider.onPostLayout();
+ assertEquals(Insets.of(0, 90, 0, 0), mProvider.getInsetsHint());
}
@Test
public void testPostLayout_invisible() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
+ statusBar.setBounds(0, 0, 500, 1000);
statusBar.getFrame().set(0, 0, 500, 100);
mProvider.setWindowContainer(statusBar, null, null);
mProvider.updateSourceFrame(statusBar.getFrame());
mProvider.onPostLayout();
- assertEquals(Insets.NONE, mProvider.getSource().calculateInsets(new Rect(0, 0, 500, 500),
- false /* ignoreVisibility */));
+ assertTrue(mProvider.getSource().getFrame().isEmpty());
+ assertEquals(Insets.NONE, mProvider.getInsetsHint());
}
@Test
@@ -160,6 +163,36 @@ public class InsetsSourceProviderTest extends WindowTestsBase {
}
@Test
+ public void testUpdateSourceFrame() {
+ final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
+ mProvider.setWindowContainer(statusBar, null, null);
+ statusBar.setBounds(0, 0, 500, 1000);
+
+ mProvider.setServerVisible(true);
+ statusBar.getFrame().set(0, 0, 500, 100);
+ mProvider.updateSourceFrame(statusBar.getFrame());
+ assertEquals(statusBar.getFrame(), mProvider.getSource().getFrame());
+ assertEquals(Insets.of(0, 100, 0, 0), mProvider.getInsetsHint());
+
+ // Only change the source frame but not the visibility.
+ statusBar.getFrame().set(0, 0, 500, 90);
+ mProvider.updateSourceFrame(statusBar.getFrame());
+ assertEquals(statusBar.getFrame(), mProvider.getSource().getFrame());
+ assertEquals(Insets.of(0, 90, 0, 0), mProvider.getInsetsHint());
+
+ mProvider.setServerVisible(false);
+ statusBar.getFrame().set(0, 0, 500, 80);
+ mProvider.updateSourceFrame(statusBar.getFrame());
+ assertTrue(mProvider.getSource().getFrame().isEmpty());
+ assertEquals(Insets.of(0, 90, 0, 0), mProvider.getInsetsHint());
+
+ // Only change the visibility but not the frame.
+ mProvider.setServerVisible(true);
+ assertEquals(statusBar.getFrame(), mProvider.getSource().getFrame());
+ assertEquals(Insets.of(0, 80, 0, 0), mProvider.getInsetsHint());
+ }
+
+ @Test
public void testUpdateSourceFrameForIme() {
final WindowState inputMethod = createWindow(null, TYPE_INPUT_METHOD, "inputMethod");
@@ -184,7 +217,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase {
}
@Test
- public void testInsetsModified() {
+ public void testSetRequestedVisibleTypes() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
final WindowState target = createWindow(null, TYPE_APPLICATION, "target");
statusBar.getFrame().set(0, 0, 500, 100);
@@ -196,7 +229,7 @@ public class InsetsSourceProviderTest extends WindowTestsBase {
}
@Test
- public void testInsetsModified_noControl() {
+ public void testSetRequestedVisibleTypes_noControl() {
final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar");
final WindowState target = createWindow(null, TYPE_APPLICATION, "target");
statusBar.getFrame().set(0, 0, 500, 100);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
index 51a7e747afce..06033c7ebf75 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -20,7 +20,6 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
-import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -42,13 +41,26 @@ import org.junit.Test;
import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
+import java.util.function.Supplier;
+/**
+ * Tests for the {@link LetterboxConfigurationPersister} class.
+ *
+ * Build/Install/Run:
+ * atest WmTests:LetterboxConfigurationPersisterTest
+ */
@SmallTest
@Presubmit
public class LetterboxConfigurationPersisterTest {
private static final long TIMEOUT = 2000L; // 2 secs
+ private static final int DEFAULT_REACHABILITY_TEST = -1;
+ private static final Supplier<Integer> DEFAULT_REACHABILITY_SUPPLIER_TEST =
+ () -> DEFAULT_REACHABILITY_TEST;
+
+ private static final String LETTERBOX_CONFIGURATION_TEST_FILENAME = "letterbox_config_test";
+
private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
private Context mContext;
private PersisterQueue mPersisterQueue;
@@ -62,7 +74,7 @@ public class LetterboxConfigurationPersisterTest {
mConfigFolder = mContext.getFilesDir();
mPersisterQueue = new PersisterQueue();
mQueueState = new QueueState();
- mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext,
+ mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(
() -> mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForHorizontalReachability),
() -> mContext.getResources().getInteger(
@@ -72,7 +84,8 @@ public class LetterboxConfigurationPersisterTest {
() -> mContext.getResources().getInteger(
R.integer.config_letterboxDefaultPositionForTabletopModeReachability
),
- mConfigFolder, mPersisterQueue, mQueueState);
+ mConfigFolder, mPersisterQueue, mQueueState,
+ LETTERBOX_CONFIGURATION_TEST_FILENAME);
mQueueListener = queueEmpty -> mQueueState.onItemAdded();
mPersisterQueue.addListener(mQueueListener);
mLetterboxConfigurationPersister.start();
@@ -127,8 +140,10 @@ public class LetterboxConfigurationPersisterTest {
public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
final PersisterQueue firstPersisterQueue = new PersisterQueue();
final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
- mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
- firstPersisterQueue, mQueueState);
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ mContext.getFilesDir(), firstPersisterQueue, mQueueState,
+ LETTERBOX_CONFIGURATION_TEST_FILENAME);
firstPersister.start();
firstPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
@@ -138,8 +153,10 @@ public class LetterboxConfigurationPersisterTest {
stopPersisterSafe(firstPersisterQueue);
final PersisterQueue secondPersisterQueue = new PersisterQueue();
final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
- mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(),
- secondPersisterQueue, mQueueState);
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ mContext.getFilesDir(), secondPersisterQueue, mQueueState,
+ LETTERBOX_CONFIGURATION_TEST_FILENAME);
secondPersister.start();
final int newPositionForHorizontalReachability =
secondPersister.getLetterboxPositionForHorizontalReachability(false);
@@ -156,37 +173,46 @@ public class LetterboxConfigurationPersisterTest {
@Test
public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
- mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false,
+ final PersisterQueue firstPersisterQueue = new PersisterQueue();
+ final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ mContext.getFilesDir(), firstPersisterQueue, mQueueState,
+ LETTERBOX_CONFIGURATION_TEST_FILENAME);
+ firstPersister.start();
+ firstPersister.setLetterboxPositionForHorizontalReachability(false,
LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
- mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false,
+ firstPersister.setLetterboxPositionForVerticalReachability(false,
LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
waitForCompletion(mPersisterQueue);
final int newPositionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
- false);
+ firstPersister.getLetterboxPositionForHorizontalReachability(false);
final int newPositionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
+ firstPersister.getLetterboxPositionForVerticalReachability(false);
Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
newPositionForHorizontalReachability);
Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
newPositionForVerticalReachability);
- deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
- waitForCompletion(mPersisterQueue);
+ deleteConfiguration(firstPersister, firstPersisterQueue);
+ waitForCompletion(firstPersisterQueue);
+ stopPersisterSafe(firstPersisterQueue);
+
+ final PersisterQueue secondPersisterQueue = new PersisterQueue();
+ final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST,
+ mContext.getFilesDir(), secondPersisterQueue, mQueueState,
+ LETTERBOX_CONFIGURATION_TEST_FILENAME);
+ secondPersister.start();
final int positionForHorizontalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability(
- false);
- final int defaultPositionForHorizontalReachability =
- mContext.getResources().getInteger(
- R.integer.config_letterboxDefaultPositionForHorizontalReachability);
- Assert.assertEquals(defaultPositionForHorizontalReachability,
- positionForHorizontalReachability);
+ secondPersister.getLetterboxPositionForHorizontalReachability(false);
final int positionForVerticalReachability =
- mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false);
- final int defaultPositionForVerticalReachability =
- mContext.getResources().getInteger(
- R.integer.config_letterboxDefaultPositionForVerticalReachability);
- Assert.assertEquals(defaultPositionForVerticalReachability,
- positionForVerticalReachability);
+ secondPersister.getLetterboxPositionForVerticalReachability(false);
+ Assert.assertEquals(DEFAULT_REACHABILITY_TEST, positionForHorizontalReachability);
+ Assert.assertEquals(DEFAULT_REACHABILITY_TEST, positionForVerticalReachability);
+ deleteConfiguration(secondPersister, secondPersisterQueue);
+ waitForCompletion(secondPersisterQueue);
+ stopPersisterSafe(secondPersisterQueue);
}
private void stopPersisterSafe(PersisterQueue persisterQueue) {
@@ -222,7 +248,7 @@ public class LetterboxConfigurationPersisterTest {
private void deleteConfiguration(LetterboxConfigurationPersister persister,
PersisterQueue persisterQueue) {
final AtomicFile fileToDelete = new AtomicFile(
- new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME));
+ new File(mConfigFolder, LETTERBOX_CONFIGURATION_TEST_FILENAME));
persisterQueue.addItem(
new DeleteFileCommand(fileToDelete, mQueueState.andThen(
s -> persister.useDefaultValue())), true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index b02b774da86a..df0808f72c3f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -1315,6 +1315,26 @@ public class RecentTasksTest extends WindowTestsBase {
assertTrue(info.supportsMultiWindow);
}
+ @Test
+ public void testRemoveCompatibleRecentTask() {
+ final Task task1 = createTaskBuilder(".Task").setWindowingMode(
+ WINDOWING_MODE_FULLSCREEN).build();
+ mRecentTasks.add(task1);
+ final Task task2 = createTaskBuilder(".Task").setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).build();
+ mRecentTasks.add(task2);
+ assertEquals(2, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
+ true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
+
+ // Set windowing mode and ensure the same fullscreen task that created earlier is removed.
+ task2.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ mRecentTasks.removeCompatibleRecentTask(task2);
+ assertEquals(1, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
+ true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
+ assertEquals(task2.mTaskId, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
+ true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().get(0).taskId);
+ }
+
private TaskSnapshot createSnapshot(Point taskSize, Point bufferSize) {
HardwareBuffer buffer = null;
if (bufferSize != null) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
index abf21a57dd40..7eab06ac8b95 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootTaskTests.java
@@ -656,7 +656,7 @@ public class RootTaskTests extends WindowTestsBase {
topSplitPrimary.getVisibility(null /* starting */));
// Make primary split root transient-hide.
spyOn(splitPrimary.mTransitionController);
- doReturn(true).when(splitPrimary.mTransitionController).isTransientHide(
+ doReturn(true).when(splitPrimary.mTransitionController).isTransientVisible(
organizer.mPrimary);
// The split root and its top become visible.
assertEquals(TASK_FRAGMENT_VISIBILITY_VISIBLE,
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 3908947804cd..d5afe3b2f078 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -27,6 +27,11 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static android.provider.DeviceConfig.NAMESPACE_CONSTRAIN_DISPLAY_APIS;
@@ -91,9 +96,12 @@ import android.compat.testing.PlatformCompatChangeRule;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Binder;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.provider.DeviceConfig;
@@ -2255,6 +2263,169 @@ public class SizeCompatTests extends WindowTestsBase {
}
@Test
+ public void testUserOverrideSplitScreenAspectRatioForLandscapeDisplay() {
+ final int displayWidth = 1600;
+ final int displayHeight = 1400;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+ float expectedAspectRatio = 1f * displayHeight / getExpectedSplitSize(displayWidth);
+
+ testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
+ }
+
+ @Test
+ public void testUserOverrideSplitScreenAspectRatioForPortraitDisplay() {
+ final int displayWidth = 1400;
+ final int displayHeight = 1600;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+ float expectedAspectRatio = 1f * displayWidth / getExpectedSplitSize(displayHeight);
+
+ testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
+ }
+
+ @Test
+ public void testUserOverrideDisplaySizeAspectRatioForLandscapeDisplay() {
+ final int displayWidth = 1600;
+ final int displayHeight = 1400;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+ float expectedAspectRatio = 1f * displayWidth / displayHeight;
+
+ testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
+ }
+
+ @Test
+ public void testUserOverrideDisplaySizeAspectRatioForPortraitDisplay() {
+ final int displayWidth = 1400;
+ final int displayHeight = 1600;
+ setUpDisplaySizeWithApp(displayWidth, displayHeight);
+
+ float expectedAspectRatio = 1f * displayHeight / displayWidth;
+
+ testUserOverrideAspectRatio(expectedAspectRatio, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
+ }
+
+ @Test
+ public void testUserOverride32AspectRatioForPortraitDisplay() {
+ setUpDisplaySizeWithApp(/* dw */ 1400, /* dh */ 1600);
+ testUserOverrideAspectRatio(3 / 2f, USER_MIN_ASPECT_RATIO_3_2);
+ }
+
+ @Test
+ public void testUserOverride32AspectRatioForLandscapeDisplay() {
+ setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+ testUserOverrideAspectRatio(3 / 2f, USER_MIN_ASPECT_RATIO_3_2);
+ }
+
+ @Test
+ public void testUserOverride43AspectRatioForPortraitDisplay() {
+ setUpDisplaySizeWithApp(/* dw */ 1400, /* dh */ 1600);
+ testUserOverrideAspectRatio(4 / 3f, USER_MIN_ASPECT_RATIO_4_3);
+ }
+
+ @Test
+ public void testUserOverride43AspectRatioForLandscapeDisplay() {
+ setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+ testUserOverrideAspectRatio(4 / 3f, USER_MIN_ASPECT_RATIO_4_3);
+ }
+
+ @Test
+ public void testUserOverride169AspectRatioForPortraitDisplay() {
+ setUpDisplaySizeWithApp(/* dw */ 1800, /* dh */ 1500);
+ testUserOverrideAspectRatio(16 / 9f, USER_MIN_ASPECT_RATIO_16_9);
+ }
+
+ @Test
+ public void testUserOverride169AspectRatioForLandscapeDisplay() {
+ setUpDisplaySizeWithApp(/* dw */ 1500, /* dh */ 1800);
+ testUserOverrideAspectRatio(16 / 9f, USER_MIN_ASPECT_RATIO_16_9);
+ }
+
+ @Test
+ @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
+ ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE})
+ public void testUserOverrideAspectRatioOverSystemOverride() {
+ setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+
+ testUserOverrideAspectRatio(false,
+ SCREEN_ORIENTATION_PORTRAIT,
+ 3 / 2f,
+ USER_MIN_ASPECT_RATIO_3_2,
+ true);
+ }
+
+ @Test
+ public void testUserOverrideAspectRatioNotEnabled() {
+ setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
+
+ // App aspect ratio doesn't change
+ testUserOverrideAspectRatio(false,
+ SCREEN_ORIENTATION_PORTRAIT,
+ 1f * 1600 / 1400,
+ USER_MIN_ASPECT_RATIO_3_2,
+ false);
+ }
+
+ private void testUserOverrideAspectRatio(float expectedAspectRatio,
+ @PackageManager.UserMinAspectRatio int aspectRatio) {
+ testUserOverrideAspectRatio(true,
+ SCREEN_ORIENTATION_PORTRAIT,
+ expectedAspectRatio,
+ aspectRatio,
+ true);
+
+ testUserOverrideAspectRatio(false,
+ SCREEN_ORIENTATION_PORTRAIT,
+ expectedAspectRatio,
+ aspectRatio,
+ true);
+
+ testUserOverrideAspectRatio(true,
+ SCREEN_ORIENTATION_LANDSCAPE,
+ expectedAspectRatio,
+ aspectRatio,
+ true);
+
+ testUserOverrideAspectRatio(false,
+ SCREEN_ORIENTATION_LANDSCAPE,
+ expectedAspectRatio,
+ aspectRatio,
+ true);
+ }
+
+ private void testUserOverrideAspectRatio(boolean isUnresizable, int screenOrientation,
+ float expectedAspectRatio, @PackageManager.UserMinAspectRatio int aspectRatio,
+ boolean enabled) {
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setTask(mTask)
+ .setComponent(ComponentName.createRelative(mContext,
+ SizeCompatTests.class.getName()))
+ .setUid(android.os.Process.myUid())
+ .build();
+ activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+ activity.mWmService.mLetterboxConfiguration
+ .setUserAppAspectRatioSettingsOverrideEnabled(enabled);
+ // Set user aspect ratio override
+ final IPackageManager pm = mAtm.getPackageManager();
+ try {
+ doReturn(aspectRatio).when(pm)
+ .getUserMinAspectRatio(activity.packageName, activity.mUserId);
+ } catch (RemoteException ignored) {
+ }
+
+ prepareLimitedBounds(activity, screenOrientation, isUnresizable);
+
+ final Rect afterBounds = activity.getBounds();
+ final int width = afterBounds.width();
+ final int height = afterBounds.height();
+ final float afterAspectRatio =
+ (float) Math.max(width, height) / (float) Math.min(width, height);
+
+ assertEquals(expectedAspectRatio, afterAspectRatio, 0.001f);
+ }
+
+ @Test
@EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO,
ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_TO_ALIGN_WITH_SPLIT_SCREEN})
public void testOverrideSplitScreenAspectRatioForUnresizablePortraitApps() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index fc5e9cab5447..810cbe8f8080 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -40,9 +40,7 @@ import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.spy;
import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
import android.view.SurfaceControl;
-import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -333,8 +331,7 @@ public class SyncEngineTests extends WindowTestsBase {
w.reparent(botChildWC, POSITION_TOP);
parentWC.prepareSync();
// Assume the window has drawn with the latest configuration.
- w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
- new MergedConfiguration(), true /* useLatestConfig */, true /* relayoutVisible */);
+ makeLastConfigReportedToClient(w, true /* visible */);
assertTrue(w.onSyncFinishedDrawing());
assertEquals(SYNC_STATE_READY, w.mSyncState);
w.reparent(topChildWC, POSITION_TOP);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 9d597b11120d..6a9bb6c85c70 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -653,4 +653,23 @@ public class TaskFragmentTest extends WindowTestsBase {
assertEquals(mDisplayContent.getImeContainer().getParent().getSurfaceControl(),
mDisplayContent.computeImeParent());
}
+
+ @Test
+ public void testIsolatedNavigation() {
+ final Task task = createTask(mDisplayContent);
+ final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+ .setParentTask(task)
+ .createActivityCount(1)
+ .setOrganizer(mOrganizer)
+ .setFragmentToken(new Binder())
+ .build();
+
+ // Cannot be isolated if not embedded.
+ task.setIsolatedNav(true);
+ assertFalse(task.isIsolatedNav());
+
+ // Ensure the TaskFragment is isolated once set.
+ tf0.setIsolatedNav(true);
+ assertTrue(tf0.isIsolatedNav());
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index ed0c8ef489e5..ca5d8fe33dba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -61,6 +61,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -1239,6 +1240,27 @@ public class TransitionTests extends WindowTestsBase {
}
@Test
+ public void testFinishRotationControllerWithFixedRotation() {
+ final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
+ registerTestTransitionPlayer();
+ mDisplayContent.setLastHasContent();
+ mDisplayContent.requestChangeTransitionIfNeeded(1 /* changes */, null /* displayChange */);
+ assertNotNull(mDisplayContent.getAsyncRotationController());
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(null);
+ assertNull("Clear rotation controller if rotation is not changed",
+ mDisplayContent.getAsyncRotationController());
+
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(app);
+ assertNotNull(mDisplayContent.getAsyncRotationController());
+ mDisplayContent.getDisplayRotation().setRotation(
+ mDisplayContent.getWindowConfiguration().getRotation() + 1);
+ mDisplayContent.setFixedRotationLaunchingAppUnchecked(null);
+ assertNotNull("Keep rotation controller if rotation will be changed",
+ mDisplayContent.getAsyncRotationController());
+ }
+
+ @Test
public void testDeferRotationForTransientLaunch() {
final TestTransitionPlayer player = registerTestTransitionPlayer();
assumeFalse(mDisplayContent.mTransitionController.useShellTransitionsRotation());
@@ -1425,6 +1447,15 @@ public class TransitionTests extends WindowTestsBase {
// No need to wait for the activity in transient hide task.
assertEquals(WindowContainer.SYNC_STATE_NONE, activity1.mSyncState);
+ // An active transient launch overrides idle state to avoid clearing power mode before the
+ // transition is finished.
+ spyOn(mRootWindowContainer.mTransitionController);
+ doAnswer(invocation -> controller.isTransientLaunch(invocation.getArgument(0))).when(
+ mRootWindowContainer.mTransitionController).isTransientLaunch(any());
+ activity2.getTask().setResumedActivity(activity2, "test");
+ activity2.idle = true;
+ assertFalse(mRootWindowContainer.allResumedActivitiesIdle());
+
activity1.setVisibleRequested(false);
activity2.setVisibleRequested(true);
activity2.setVisible(true);
@@ -1474,6 +1505,47 @@ public class TransitionTests extends WindowTestsBase {
}
@Test
+ public void testIsTransientVisible() {
+ final ActivityRecord appB = new ActivityBuilder(mAtm).setCreateTask(true)
+ .setVisible(false).build();
+ final ActivityRecord recent = new ActivityBuilder(mAtm).setCreateTask(true)
+ .setVisible(false).build();
+ final ActivityRecord appA = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ final Task taskA = appA.getTask();
+ final Task taskB = appB.getTask();
+ final Task taskRecent = recent.getTask();
+ registerTestTransitionPlayer();
+ final TransitionController controller = mRootWindowContainer.mTransitionController;
+ final Transition transition = createTestTransition(TRANSIT_OPEN, controller);
+ controller.moveToCollecting(transition);
+ transition.collect(recent);
+ transition.collect(taskA);
+ transition.setTransientLaunch(recent, taskA);
+ taskRecent.moveToFront("move-recent-to-front");
+
+ // During collecting and playing, the recent is on top so it is visible naturally.
+ // While B needs isTransientVisible to keep visibility because it is occluded by recents.
+ assertFalse(controller.isTransientVisible(taskB));
+ assertTrue(controller.isTransientVisible(taskA));
+ assertFalse(controller.isTransientVisible(taskRecent));
+ // Switch to playing state.
+ transition.onTransactionReady(transition.getSyncId(), mMockT);
+ assertTrue(controller.isTransientVisible(taskA));
+
+ // Switch to another task. For example, use gesture navigation to switch tasks.
+ taskB.moveToFront("move-b-to-front");
+ // The previous app (taskA) should be paused first so it loses transient visible. Because
+ // visually it is taskA -> taskB, the pause -> resume order should be the same.
+ assertFalse(controller.isTransientVisible(taskA));
+ // Keep the recent visible so there won't be 2 activities pausing at the same time. It is
+ // to avoid the latency to resume the current top, i.e. appB.
+ assertTrue(controller.isTransientVisible(taskRecent));
+ // The recent is paused after the transient transition is finished.
+ controller.finishTransition(transition);
+ assertFalse(controller.isTransientVisible(taskRecent));
+ }
+
+ @Test
public void testNotReadyPushPop() {
final TransitionController controller = new TestTransitionController(mAtm);
controller.setSyncEngine(mWm.mSyncEngine);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 4afcd0539b94..e3d1b9c669f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -52,7 +52,6 @@ import android.graphics.Rect;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-import android.util.MergedConfiguration;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.DisplayShape;
@@ -63,7 +62,6 @@ import android.view.RoundedCorners;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
-import android.window.ClientWindowFrames;
import androidx.test.filters.SmallTest;
@@ -380,8 +378,7 @@ public class WallpaperControllerTests extends WindowTestsBase {
wallpaperWindow.mLayoutSeq = mDisplayContent.mLayoutSeq;
// Assume the token was invisible and the latest config was reported.
wallpaperToken.commitVisibility(false);
- wallpaperWindow.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
- new MergedConfiguration(), true /* useLatestConfig */, false /* relayoutVisible */);
+ makeLastConfigReportedToClient(wallpaperWindow, false /* visible */);
assertTrue(wallpaperWindow.isLastConfigReportedToClient());
final Rect bounds = wallpaperToken.getBounds();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 600681fb332c..7168670f9652 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1247,6 +1247,7 @@ public class WindowOrganizerTests extends WindowTestsBase {
// A drawn window can complete the sync state automatically.
w1.mWinAnimator.mDrawState = WindowStateAnimator.HAS_DRAWN;
+ makeLastConfigReportedToClient(w1, true /* visible */);
mWm.mSyncEngine.onSurfacePlacement();
verify(mockCallback).onTransactionReady(anyInt(), any());
assertFalse(w1.useBLASTSync());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index d5547ec69247..873c7f4e0e30 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -85,6 +85,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
+import android.util.MergedConfiguration;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
@@ -102,6 +103,7 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowManager.DisplayImePolicy;
import android.view.inputmethod.ImeTracker;
+import android.window.ClientWindowFrames;
import android.window.ITransitionPlayer;
import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
@@ -624,6 +626,11 @@ class WindowTestsBase extends SystemServiceTestsBase {
}
}
+ static void makeLastConfigReportedToClient(WindowState w, boolean visible) {
+ w.fillClientWindowFramesAndConfiguration(new ClientWindowFrames(),
+ new MergedConfiguration(), true /* useLatestConfig */, visible);
+ }
+
/**
* Gets the order of the given {@link Task} as its z-order in the hierarchy below this TDA.
* The Task can be a direct child of a child TaskDisplayArea. {@code -1} if not found.
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 248cc26ce656..ccc4ac28876a 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -27,6 +27,7 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPH
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER;
import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH;
+import android.app.AppOpsManager;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
@@ -548,13 +549,15 @@ final class HotwordDetectionConnection {
static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub {
private final HotwordDetectionConnection mHotwordDetectionConnection;
private final IHotwordRecognitionStatusCallback mExternalCallback;
- private final int mVoiceInteractionServiceUid;
+ private final Identity mVoiceInteractorIdentity;
+ private final Context mContext;
- SoundTriggerCallback(IHotwordRecognitionStatusCallback callback,
- HotwordDetectionConnection connection, int uid) {
+ SoundTriggerCallback(Context context, IHotwordRecognitionStatusCallback callback,
+ HotwordDetectionConnection connection, Identity voiceInteractorIdentity) {
+ mContext = context;
mHotwordDetectionConnection = connection;
mExternalCallback = callback;
- mVoiceInteractionServiceUid = uid;
+ mVoiceInteractorIdentity = voiceInteractorIdentity;
}
@Override
@@ -568,15 +571,30 @@ final class HotwordDetectionConnection {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP,
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
- mVoiceInteractionServiceUid);
+ mVoiceInteractorIdentity.uid);
mHotwordDetectionConnection.detectFromDspSource(
recognitionEvent, mExternalCallback);
} else {
- HotwordMetricsLogger.writeKeyphraseTriggerEvent(
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
- mVoiceInteractionServiceUid);
- mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
+ // We have to attribute ops here, since we configure all st clients as trusted to
+ // enable a partial exemption.
+ // TODO (b/292012931) remove once trusted uniformly required.
+ int result = mContext.getSystemService(AppOpsManager.class)
+ .noteOpNoThrow(AppOpsManager.OP_RECORD_AUDIO_HOTWORD,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag,
+ "Non-HDS keyphrase recognition to VoiceInteractionService");
+
+ if (result != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(TAG, "onKeyphraseDetected suppressed, permission check returned: "
+ + result);
+ mExternalCallback.onRecognitionPaused();
+ } else {
+ HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+ mVoiceInteractorIdentity.uid);
+ mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
+ }
}
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 423a81ac0523..3574ef8e91fb 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -104,6 +104,7 @@ import com.android.server.SystemService;
import com.android.server.UiThread;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.permission.LegacyPermissionManagerInternal;
+import com.android.server.policy.AppOpsPolicy;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -336,6 +337,9 @@ public class VoiceInteractionManagerService extends SystemService {
/** The start value of showSessionId */
private static final int SHOW_SESSION_START_ID = 0;
+ private final boolean IS_HDS_REQUIRED = AppOpsPolicy.isHotwordDetectionServiceRequired(
+ mContext.getPackageManager());
+
@GuardedBy("this")
private int mShowSessionId = SHOW_SESSION_START_ID;
@@ -393,8 +397,14 @@ public class VoiceInteractionManagerService extends SystemService {
}
try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
originatorIdentity)) {
+ if (!IS_HDS_REQUIRED) {
+ // For devices which still have hotword exemption, any client (not just HDS
+ // clients) are trusted.
+ // TODO (b/292012931) remove once trusted uniformly required.
+ forHotwordDetectionService = true;
+ }
return new SoundTriggerSession(mSoundTriggerInternal.attach(client,
- moduleProperties, forHotwordDetectionService));
+ moduleProperties, forHotwordDetectionService), originatorIdentity);
}
}
@@ -1674,10 +1684,13 @@ public class VoiceInteractionManagerService extends SystemService {
final SoundTriggerInternal.Session mSession;
private IHotwordRecognitionStatusCallback mSessionExternalCallback;
private IRecognitionStatusCallback mSessionInternalCallback;
+ private final Identity mVoiceInteractorIdentity;
SoundTriggerSession(
- SoundTriggerInternal.Session session) {
+ SoundTriggerInternal.Session session,
+ Identity voiceInteractorIdentity) {
mSession = session;
+ mVoiceInteractorIdentity = voiceInteractorIdentity;
}
@Override
@@ -1731,7 +1744,8 @@ public class VoiceInteractionManagerService extends SystemService {
if (mSessionExternalCallback == null
|| mSessionInternalCallback == null
|| callback.asBinder() != mSessionExternalCallback.asBinder()) {
- mSessionInternalCallback = createSoundTriggerCallbackLocked(callback);
+ mSessionInternalCallback = createSoundTriggerCallbackLocked(callback,
+ mVoiceInteractorIdentity);
mSessionExternalCallback = callback;
}
}
@@ -1752,7 +1766,8 @@ public class VoiceInteractionManagerService extends SystemService {
if (mSessionExternalCallback == null
|| mSessionInternalCallback == null
|| callback.asBinder() != mSessionExternalCallback.asBinder()) {
- soundTriggerCallback = createSoundTriggerCallbackLocked(callback);
+ soundTriggerCallback = createSoundTriggerCallbackLocked(callback,
+ mVoiceInteractorIdentity);
Slog.w(TAG, "stopRecognition() called with a different callback than"
+ "startRecognition()");
} else {
@@ -2090,6 +2105,7 @@ public class VoiceInteractionManagerService extends SystemService {
pw.println(" mTemporarilyDisabled: " + mTemporarilyDisabled);
pw.println(" mCurUser: " + mCurUser);
pw.println(" mCurUserSupported: " + mCurUserSupported);
+ pw.println(" mIsHdsRequired: " + IS_HDS_REQUIRED);
dumpSupportedUsers(pw, " ");
mDbHelper.dump(pw);
if (mImpl == null) {
@@ -2165,11 +2181,13 @@ public class VoiceInteractionManagerService extends SystemService {
}
private IRecognitionStatusCallback createSoundTriggerCallbackLocked(
- IHotwordRecognitionStatusCallback callback) {
+ IHotwordRecognitionStatusCallback callback,
+ Identity voiceInteractorIdentity) {
if (mImpl == null) {
return null;
}
- return mImpl.createSoundTriggerCallbackLocked(callback);
+ return mImpl.createSoundTriggerCallbackLocked(mContext, callback,
+ voiceInteractorIdentity);
}
class RoleObserver implements OnRoleHoldersChangedListener {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 0ad86c11d29a..5d88a65ce29e 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -877,12 +877,13 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne
}
public IRecognitionStatusCallback createSoundTriggerCallbackLocked(
- IHotwordRecognitionStatusCallback callback) {
+ Context context, IHotwordRecognitionStatusCallback callback,
+ Identity voiceInteractorIdentity) {
if (DEBUG) {
Slog.d(TAG, "createSoundTriggerCallbackLocked");
}
- return new HotwordDetectionConnection.SoundTriggerCallback(callback,
- mHotwordDetectionConnection, mInfo.getServiceInfo().applicationInfo.uid);
+ return new HotwordDetectionConnection.SoundTriggerCallback(context, callback,
+ mHotwordDetectionConnection, voiceInteractorIdentity);
}
private static ServiceInfo getServiceInfoLocked(@NonNull ComponentName componentName,
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 3e7919305cbf..3703349b8d7e 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9400,6 +9400,39 @@ public class CarrierConfigManager {
"missed_incoming_call_sms_pattern_string_array";
/**
+ * Indicate the satellite services supported per provider by a carrier.
+ *
+ * Key is the PLMN of a satellite provider. Value should be an integer array of supported
+ * services with the following value:
+ * <ul>
+ * <li>1 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_VOICE}</li>
+ * <li>2 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_DATA}</li>
+ * <li>3 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_SMS}</li>
+ * <li>4 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_VIDEO}</li>
+ * <li>5 = {@link android.telephony.NetworkRegistrationInfo#SERVICE_TYPE_EMERGENCY}</li>
+ * </ul>
+ * <p>
+ * If this carrier config is not present, the overlay config
+ * {@code config_satellite_services_supported_by_providers} will be used. If the carrier config
+ * is present, the supported satellite services will be identified as follows:
+ * <ul>
+ * <li>For the PLMN that exists in both provider supported satellite services and carrier
+ * supported satellite services, the supported services will be the intersection of the two
+ * sets.</li>
+ * <li>For the PLMN that is present in provider supported satellite services but not in carrier
+ * supported satellite services, the provider supported satellite services will be used.</li>
+ * <li>For the PLMN that is present in carrier supported satellite services but not in provider
+ * supported satellite services, the PLMN will be ignored.</li>
+ * </ul>
+ *
+ * This config is empty by default.
+ *
+ * @hide
+ */
+ public static final String KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE =
+ "carrier_supported_satellite_services_per_provider_bundle";
+
+ /**
* Indicating whether DUN APN should be disabled when the device is roaming. In that case,
* the default APN (i.e. internet) will be used for tethering.
*
@@ -9621,6 +9654,7 @@ public class CarrierConfigManager {
*
* @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_CANCELED
* @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_TIMEOUT
+ * @see TelephonyManager#PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED
*/
public static final String
KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG =
@@ -10404,6 +10438,9 @@ public class CarrierConfigManager {
});
sDefaults.putBoolean(KEY_DELAY_IMS_TEAR_DOWN_UNTIL_CALL_END_BOOL, false);
sDefaults.putStringArray(KEY_MISSED_INCOMING_CALL_SMS_PATTERN_STRING_ARRAY, new String[0]);
+ sDefaults.putPersistableBundle(
+ KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE,
+ PersistableBundle.EMPTY);
sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java
index 182d2fcbec67..f012ab56d94d 100644
--- a/telephony/java/android/telephony/NetworkRegistrationInfo.java
+++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java
@@ -203,6 +203,12 @@ public final class NetworkRegistrationInfo implements Parcelable {
*/
public static final int SERVICE_TYPE_EMERGENCY = 5;
+ /** @hide */
+ public static final int FIRST_SERVICE_TYPE = SERVICE_TYPE_VOICE;
+
+ /** @hide */
+ public static final int LAST_SERVICE_TYPE = SERVICE_TYPE_EMERGENCY;
+
@Domain
private final int mDomain;
@@ -240,7 +246,7 @@ public final class NetworkRegistrationInfo implements Parcelable {
private final boolean mEmergencyOnly;
@ServiceType
- private final ArrayList<Integer> mAvailableServices;
+ private ArrayList<Integer> mAvailableServices;
@Nullable
private CellIdentity mCellIdentity;
@@ -604,6 +610,16 @@ public final class NetworkRegistrationInfo implements Parcelable {
}
/**
+ * Set available service types.
+ *
+ * @param availableServices The list of available services for this network.
+ * @hide
+ */
+ public void setAvailableServices(@NonNull @ServiceType List<Integer> availableServices) {
+ mAvailableServices = new ArrayList<>(availableServices);
+ }
+
+ /**
* @return The access network technology {@link NetworkType}.
*/
public @NetworkType int getAccessNetworkTechnology() {
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 64e43568e4d6..8e4ec0914563 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -151,6 +151,15 @@ public class SubscriptionManager {
"restoreSimSpecificSettings";
/**
+ * The key of the boolean flag indicating whether restoring subscriptions actually changes
+ * the subscription database or not.
+ *
+ * @hide
+ */
+ public static final String RESTORE_SIM_SPECIFIC_SETTINGS_DATABASE_UPDATED =
+ "restoreSimSpecificSettingsDatabaseUpdated";
+
+ /**
* Key to the backup & restore data byte array in the Bundle that is returned by {@link
* #getAllSimSpecificSettingsForBackup()} or to be pass in to {@link
* #restoreAllSimSpecificSettingsFromBackup(byte[])}.
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 2a6099a18fab..340e4ab132ca 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -17608,6 +17608,16 @@ public class TelephonyManager {
public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP = 15;
/**
+ * Purchase premium capability failed because the user disabled the feature.
+ * Subsequent attempts will be throttled for the amount of time specified by
+ * {@link CarrierConfigManager
+ * #KEY_PREMIUM_CAPABILITY_NOTIFICATION_BACKOFF_HYSTERESIS_TIME_MILLIS_LONG}
+ * and return {@link #PURCHASE_PREMIUM_CAPABILITY_RESULT_THROTTLED}.
+ * @hide
+ */
+ public static final int PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED = 16;
+
+ /**
* Results of the purchase premium capability request.
* @hide
*/
@@ -17626,7 +17636,8 @@ public class TelephonyManager {
PURCHASE_PREMIUM_CAPABILITY_RESULT_NETWORK_NOT_AVAILABLE,
PURCHASE_PREMIUM_CAPABILITY_RESULT_ENTITLEMENT_CHECK_FAILED,
PURCHASE_PREMIUM_CAPABILITY_RESULT_NOT_DEFAULT_DATA_SUBSCRIPTION,
- PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP})
+ PURCHASE_PREMIUM_CAPABILITY_RESULT_PENDING_NETWORK_SETUP,
+ PURCHASE_PREMIUM_CAPABILITY_RESULT_USER_DISABLED})
public @interface PurchasePremiumCapabilityResult {}
/**
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
index 45cd65d9776c..45176448a9f4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt
@@ -17,9 +17,12 @@
package com.android.server.wm.flicker.activityembedding
import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.platform.test.annotations.Presubmit
+import android.tools.common.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.BaseTest
import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
import org.junit.Before
+import org.junit.Test
abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(flicker) {
val testApp = ActivityEmbeddingAppHelper(instrumentation)
@@ -29,4 +32,14 @@ abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(
// The test should only be run on devices that support ActivityEmbedding.
ActivityEmbeddingAppHelper.assumeActivityEmbeddingSupportedDevice()
}
+
+ /** Asserts the background animation layer is never visible during bounds change transition. */
+ @Presubmit
+ @Test
+ fun backgroundLayerNeverVisible() {
+ val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
+ flicker.assertLayers {
+ isInvisible(backgroundColorLayer)
+ }
+ }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
new file mode 100644
index 000000000000..badd876ae321
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import androidx.test.filters.RequiresDevice
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test changing split ratio at runtime on a horizona split.
+ *
+ * Setup: Launch A|B in horizontal split with B being the secondary activity, by default A and B
+ * windows are equal in size. B is on the top and A is on the bottom.
+ * Transitions:
+ * Change the split ratio to A:B=0.7:0.3, expect bounds change for both A and B.
+ *
+ * To run this test: `atest FlickerTests:HorizontalSplitChangeRatioTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) :
+ ActivityEmbeddingTestBase(flicker) {
+ /** {@inheritDoc} */
+ override val transition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setExpectedRotationCheckEnabled(false)
+ testApp.launchViaIntent(wmHelper)
+ testApp.launchSecondaryActivityHorizontally(wmHelper)
+ startDisplayBounds =
+ wmHelper.currentState.layerState.physicalDisplayBounds
+ ?: error("Display not found")
+ }
+ transitions {
+ testApp.changeSecondaryActivityRatio(wmHelper)
+ }
+ teardown {
+ tapl.goHome()
+ testApp.exit(wmHelper)
+ }
+ }
+
+ /** Assert the Main activity window is always visible. */
+ @Presubmit
+ @Test
+ fun mainActivityWindowIsAlwaysVisible() {
+ flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Main activity window is always visible. */
+ @Presubmit
+ @Test
+ fun mainActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Secondary activity window is always visible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityWindowIsAlwaysVisible() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Secondary activity window is always visible. */
+ @Presubmit
+ @Test
+ fun secondaryActivityLayerIsAlwaysVisible() {
+ flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) }
+ }
+
+ /** Assert the Main and Secondary activity change height during the transition. */
+ @Presubmit
+ @Test
+ fun secondaryActivityAdjustsHeightRuntime() {
+ flicker.assertLayersStart {
+ val topLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ val bottomLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(topLayerRegion.region.height).isEqual(bottomLayerRegion.region.height)
+ check { "width" }
+ .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
+ topLayerRegion.notOverlaps(bottomLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+
+ flicker.assertLayersEnd {
+ val topLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ val bottomLayerRegion =
+ this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(topLayerRegion.region.height).isLower(bottomLayerRegion.region.height)
+ check { "height" }
+ .that(
+ topLayerRegion.region.height / 0.3f -
+ bottomLayerRegion.region.height / 0.7f)
+ .isLower(0.1f)
+ check { "width" }
+ .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width)
+ topLayerRegion.notOverlaps(bottomLayerRegion.region)
+ // Layers of two activities sum to be fullscreen size on display.
+ topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+ }
+
+ companion object {
+ /** {@inheritDoc} */
+ private var startDisplayBounds = Rect.EMPTY
+ /**
+ * Creates the test configurations.
+ *
+ * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+ }
+} \ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
index 27de12e7dfdb..404f3290f04a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt
@@ -142,14 +142,6 @@ class OpenThirdActivityOverSplitTest(flicker: LegacyFlickerTest) :
}
}
- /** Assert the background animation layer is never visible during transition. */
- @Presubmit
- @Test
- fun backgroundLayerNeverVisible() {
- val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
- flicker.assertLayers { isInvisible(backgroundColorLayer) }
- }
-
companion object {
/** {@inheritDoc} */
private var startDisplayBounds = Rect.EMPTY
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
index c05dc324ac9e..d3001d8fdcaf 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
@@ -16,6 +16,7 @@
package com.android.server.wm.flicker.activityembedding
+import android.platform.test.annotations.FlakyTest
import android.platform.test.annotations.Presubmit
import android.tools.common.datatypes.Rect
import android.tools.common.datatypes.Region
@@ -65,16 +66,6 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding
}
}
- /** Assert the background animation layer is never visible during bounds change transition. */
- @Presubmit
- @Test
- fun backgroundLayerNeverVisible() {
- val backgroundColorLayer = ComponentNameMatcher("", "Animation Background")
- flicker.assertLayers {
- isInvisible(backgroundColorLayer)
- }
- }
-
/** Trampoline activity should finish itself before the end of this test. */
@Presubmit
@Test
@@ -178,6 +169,7 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding
}
}
+ @FlakyTest(bugId = 290736037)
/** Main activity should go from fullscreen to being a split with secondary activity. */
@Presubmit
@Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
new file mode 100644
index 000000000000..0417f9dbb4bf
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import android.tools.common.datatypes.Rect
+import android.tools.common.traces.component.ComponentNameMatcher
+import android.tools.common.traces.component.ComponentNameMatcher.Companion.TRANSITION_SNAPSHOT
+import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.device.flicker.legacy.FlickerBuilder
+import android.tools.device.flicker.legacy.LegacyFlickerTest
+import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a secondary Activity into Picture-In-Picture mode.
+ *
+ * Setup: Start from a split A|B.
+ * Transition: B enters PIP, observe the window shrink to the bottom right corner on screen.
+ *
+ * To run this test: `atest FlickerTests:SecondaryActivityEnterPipTest`
+ *
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SecondaryActivityEnterPipTest (flicker: LegacyFlickerTest) :
+ ActivityEmbeddingTestBase(flicker) {
+ override val transition: FlickerBuilder.() -> Unit = {
+ setup {
+ tapl.setExpectedRotationCheckEnabled(false)
+ testApp.launchViaIntent(wmHelper)
+ testApp.launchSecondaryActivity(wmHelper)
+ startDisplayBounds =
+ wmHelper.currentState.layerState.physicalDisplayBounds
+ ?: error("Can't get display bounds")
+ }
+ transitions {
+ testApp.secondaryActivityEnterPip(wmHelper)
+ }
+ teardown {
+ tapl.goHome()
+ testApp.exit(wmHelper)
+ }
+ }
+
+ /**
+ * Main and secondary activity start from a split each taking half of the screen.
+ */
+ @Presubmit
+ @Test
+ fun layersStartFromEqualSplit() {
+ flicker.assertLayersStart {
+ val leftLayerRegion =
+ visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ val rightLayerRegion =
+ visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ // Compare dimensions of two splits, given we're using default split attributes,
+ // both activities take up the same visible size on the display.
+ check { "height" }
+ .that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height)
+ check { "width" }
+ .that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width)
+ leftLayerRegion.notOverlaps(rightLayerRegion.region)
+ leftLayerRegion.plus(rightLayerRegion.region).coversExactly(startDisplayBounds)
+ }
+ flicker.assertLayersEnd {
+ visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .coversExactly(startDisplayBounds)
+ }
+ }
+
+ /**
+ * Main Activity is visible throughout the transition and becomes fullscreen.
+ */
+ @Presubmit
+ @Test
+ fun mainActivityWindowBecomesFullScreen() {
+ flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) }
+ flicker.assertWmEnd {
+ visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .coversExactly(startDisplayBounds)
+ }
+ }
+
+ /**
+ * Main Activity is visible throughout the transition and becomes fullscreen.
+ */
+ @Presubmit
+ @Test
+ fun mainActivityLayerBecomesFullScreen() {
+ flicker.assertLayers {
+ isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .then()
+ .isVisible(TRANSITION_SNAPSHOT)
+ .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .then()
+ .isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ }
+ flicker.assertLayersEnd {
+ visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+ .coversExactly(startDisplayBounds)
+ }
+ }
+
+ /**
+ * Secondary Activity is visible throughout the transition and shrinks to the bottom right
+ * corner.
+ */
+ @Presubmit
+ @Test
+ fun secondaryWindowShrinks() {
+ flicker.assertWm {
+ isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ }
+ flicker.assertWmEnd {
+ val pipWindowRegion =
+ visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ check{"height"}
+ .that(pipWindowRegion.region.height)
+ .isLower(startDisplayBounds.height / 2)
+ check{"width"}
+ .that(pipWindowRegion.region.width).isLower(startDisplayBounds.width)
+ }
+ }
+
+ /**
+ * During the transition Secondary Activity shrinks to the bottom right corner.
+ */
+ @Presubmit
+ @Test
+ fun secondaryLayerShrinks() {
+ flicker.assertLayers {
+ val pipLayerList = layers {
+ ComponentNameMatcher.PIP_CONTENT_OVERLAY.layerMatchesAnyOf(it) && it.isVisible
+ }
+ pipLayerList.zipWithNext { previous, current ->
+ // TODO(b/290987990): Add checks for visibleRegion.
+ current.screenBounds.isToTheRightBottom(previous.screenBounds.region, 3)
+ current.screenBounds.notBiggerThan(previous.screenBounds.region)
+ }
+ }
+ flicker.assertLayersEnd {
+ val pipRegion = visibleRegion(
+ ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+ check { "height" }
+ .that(pipRegion.region.height)
+ .isLower(startDisplayBounds.height / 2)
+ check { "width" }
+ .that(pipRegion.region.width).isLower(startDisplayBounds.width)
+ }
+ }
+
+ companion object {
+ /** {@inheritDoc} */
+ private var startDisplayBounds = Rect.EMPTY
+ /**
+ * Creates the test configurations.
+ *
+ * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and
+ * navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+ }
+} \ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index eac88132d410..883c7e6d5785 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -45,18 +45,22 @@ constructor(
* based on the split pair rule.
*/
fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) {
- val launchButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")),
- FIND_TIMEOUT
- )
- require(launchButton != null) { "Can't find launch secondary activity button on screen." }
- launchButton.click()
- wmHelper
- .StateSyncBuilder()
- .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .waitForAndVerify()
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_button")
+ }
+
+ /**
+ * Clicks the button to launch the secondary activity in RTL, which should split with the main
+ * activity based on the split pair rule.
+ */
+ fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_rtl_button")
+ }
+
+ /**
+ * Clicks the button to launch the secondary activity in a horizontal split.
+ */
+ fun launchSecondaryActivityHorizontally(wmHelper: WindowManagerStateHelper) {
+ launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_horizontally_button")
}
/** Clicks the button to launch a third activity over a secondary activity. */
@@ -101,16 +105,53 @@ constructor(
*/
fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) {
val finishButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
- FIND_TIMEOUT
- )
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")),
+ FIND_TIMEOUT
+ )
require(finishButton != null) { "Can't find finish secondary activity button on screen." }
finishButton.click()
wmHelper
- .StateSyncBuilder()
- .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
- .waitForAndVerify()
+ .StateSyncBuilder()
+ .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT)
+ .waitForAndVerify()
+ }
+
+ /**
+ * Clicks the button to toggle the split ratio of secondary activity.
+ */
+ fun changeSecondaryActivityRatio(wmHelper: WindowManagerStateHelper) {
+ val launchButton =
+ uiDevice.wait(
+ Until.findObject(
+ By.res(getPackage(),
+ "toggle_split_ratio_button")),
+ FIND_TIMEOUT
+ )
+ require(launchButton != null) {
+ "Can't find toggle ratio for secondary activity button on screen."
+ }
+ launchButton.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withTransitionSnapshotGone()
+ .waitForAndVerify()
+ }
+
+ fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) {
+ val pipButton =
+ uiDevice.wait(
+ Until.findObject(By.res(getPackage(), "secondary_enter_pip_button")),
+ FIND_TIMEOUT
+ )
+ require(pipButton != null) { "Can't find enter pip button on screen." }
+ pipButton.click()
+ wmHelper
+ .StateSyncBuilder()
+ .withAppTransitionIdle()
+ .withPipShown()
+ .waitForAndVerify()
}
/**
@@ -134,25 +175,19 @@ constructor(
.waitForAndVerify()
}
- /**
- * Clicks the button to launch the secondary activity in RTL, which should split with the main
- * activity based on the split pair rule.
- */
- fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) {
+ private fun launchSecondaryActivityFromButton(
+ wmHelper: WindowManagerStateHelper, buttonName: String) {
val launchButton =
- uiDevice.wait(
- Until.findObject(By.res(getPackage(), "launch_secondary_activity_rtl_button")),
- FIND_TIMEOUT
- )
+ uiDevice.wait(Until.findObject(By.res(getPackage(), buttonName)), FIND_TIMEOUT)
require(launchButton != null) {
- "Can't find launch secondary activity rtl button on screen."
+ "Can't find launch secondary activity button : " + buttonName + "on screen."
}
launchButton.click()
wmHelper
- .StateSyncBuilder()
- .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
- .waitForAndVerify()
+ .StateSyncBuilder()
+ .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+ .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED)
+ .waitForAndVerify()
}
/**
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index c975a50ace02..c6b86f2689f0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -386,8 +386,11 @@ open class PipAppHelper(instrumentation: Instrumentation) :
it.wmState.visibleWindows.firstOrNull { window ->
this.windowMatchesAnyOf(window)
}
- ?: return@add false
+ Log.d(TAG, "window " + pipAppWindow)
+ if (pipAppWindow == null) return@add false
val pipRegion = pipAppWindow.frameRegion
+ Log.d(TAG, "region " + pipRegion +
+ " covers " + windowRect.coversMoreThan(pipRegion))
return@add windowRect.coversMoreThan(pipRegion)
}
.waitForAndVerify()
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 68ae806f3c8b..ff9799a1c710 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -102,6 +102,24 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity android:name=".LaunchTransparentActivity"
+ android:resizeableActivity="false"
+ android:screenOrientation="portrait"
+ android:theme="@android:style/Theme"
+ android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity"
+ android:label="LaunchTransparentActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ <activity android:name=".TransparentActivity"
+ android:theme="@style/TransparentTheme"
+ android:taskAffinity="com.android.server.wm.flicker.testapp.TransparentActivity"
+ android:label="TransparentActivity"
+ android:exported="false">
+ </activity>
<activity android:name=".LaunchNewActivity"
android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewActivity"
android:theme="@style/CutoutShortEdges"
@@ -206,6 +224,7 @@
android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
android:theme="@style/CutoutShortEdges"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+ android:supportsPictureInPicture="true"
android:exported="false"/>
<activity
android:name=".ActivityEmbeddingThirdActivity"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
index e32a7092bf5d..86c21906163f 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
@@ -38,6 +38,14 @@
android:text="Launch Secondary Activity in RTL" />
<Button
+ android:id="@+id/launch_secondary_activity_horizontally_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:onClick="launchSecondaryActivity"
+ android:tag="BOTTOM_TO_TOP"
+ android:text="Launch Secondary Activity Horizontally" />
+
+ <Button
android:id="@+id/launch_placeholder_split_button"
android:layout_width="wrap_content"
android:layout_height="48dp"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
index 67314463161d..6d4de995bd73 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml
@@ -35,4 +35,18 @@
android:onClick="launchThirdActivity"
android:text="Launch a third activity" />
+ <ToggleButton
+ android:id="@+id/toggle_split_ratio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:textOn="Ratio 0.5"
+ android:textOff="Ratio 0.3"
+ android:checked="false" />
+
+ <Button
+ android:id="@+id/secondary_enter_pip_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:text="Enter pip" />
+
</LinearLayout> \ No newline at end of file
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml
new file mode 100644
index 000000000000..0730ded66ce4
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+</FrameLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml
new file mode 100644
index 000000000000..ff4ead95f16e
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:background="@android:color/black">
+
+ <Button
+ android:id="@+id/button_launch_transparent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:text="Launch Transparent" />
+ <Button
+ android:id="@+id/button_request_permission"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:text="Request Permission" />
+</LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml
index 1d21fd56a487..e51ed29adebf 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml
@@ -43,6 +43,13 @@
<item name="android:windowSoftInputMode">stateUnchanged</item>
</style>
+ <style name="TransparentTheme" parent="@android:style/Theme.DeviceDefault">
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:backgroundDimEnabled">false</item>
+ </style>
+
<style name="no_starting_window" parent="@android:style/Theme.DeviceDefault">
<item name="android:windowDisablePreview">true</item>
</style>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
index 3b1a8599f3e1..23fa91c37728 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
@@ -22,6 +22,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.window.embedding.ActivityFilter;
import androidx.window.embedding.ActivityRule;
import androidx.window.embedding.EmbeddingAspectRatio;
@@ -152,6 +153,9 @@ public class ActivityEmbeddingMainActivity extends Activity {
if (layoutDirectionStr.equals(LayoutDirection.LEFT_TO_RIGHT.toString())) {
return LayoutDirection.LEFT_TO_RIGHT;
}
+ if (layoutDirectionStr.equals(LayoutDirection.BOTTOM_TO_TOP.toString())) {
+ return LayoutDirection.BOTTOM_TO_TOP;
+ }
if (layoutDirectionStr.equals(LayoutDirection.RIGHT_TO_LEFT.toString())) {
return LayoutDirection.RIGHT_TO_LEFT;
}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
index dc21027bc99c..29cbf01dc6da 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -18,9 +18,15 @@ package com.android.server.wm.flicker.testapp;
import android.app.Activity;
import android.content.Intent;
+import android.app.PictureInPictureParams;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
+import android.widget.ToggleButton;
+
+import androidx.window.embedding.SplitAttributes;
+import androidx.window.embedding.SplitAttributesCalculatorParams;
+import androidx.window.embedding.SplitController;
/**
* Activity to be used as the secondary activity to split with
@@ -28,18 +34,51 @@ import android.view.View;
*/
public class ActivityEmbeddingSecondaryActivity extends Activity {
+ private SplitController mSplitController;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_embedding_secondary_activity_layout);
findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW);
findViewById(R.id.finish_secondary_activity_button).setOnClickListener(
- new View.OnClickListener() {
+ new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
- });
+ });
+ mSplitController = SplitController.getInstance(this);
+ final ToggleButton splitRatio = findViewById(R.id.toggle_split_ratio_button);
+ mSplitController.setSplitAttributesCalculator(params -> {
+ return new SplitAttributes.Builder()
+ .setSplitType(
+ SplitAttributes.SplitType.ratio(
+ splitRatio.isChecked() ? 0.7f : 0.5f)
+ )
+ .setLayoutDirection(
+ params.getDefaultSplitAttributes()
+ .getLayoutDirection())
+ .build();
+ });
+ splitRatio.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // This triggers a recalcuation of splitatributes.
+ mSplitController.invalidateTopVisibleSplitAttributes();
+ }
+ });
+ findViewById(R.id.secondary_enter_pip_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ PictureInPictureParams.Builder picInPicParamsBuilder =
+ new PictureInPictureParams.Builder();
+ enterPictureInPictureMode(picInPicParamsBuilder.build());
+ }
+ }
+ );
}
public void launchThirdActivity(View view) {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 95c86acb9ee9..2795a6c43015 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -73,6 +73,18 @@ public class ActivityOptions {
FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity");
}
+ public static class TransparentActivity {
+ public static final String LABEL = "TransparentActivity";
+ public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+ FLICKER_APP_PACKAGE + ".TransparentActivity");
+ }
+
+ public static class LaunchTransparentActivity {
+ public static final String LABEL = "LaunchTransparentActivity";
+ public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+ FLICKER_APP_PACKAGE + ".LaunchTransparentActivity");
+ }
+
public static class DialogThemedActivity {
public static final String LABEL = "DialogThemedActivity";
public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java
new file mode 100644
index 000000000000..7c161fd8e611
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class LaunchTransparentActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.activity_transparent_launch);
+ findViewById(R.id.button_launch_transparent)
+ .setOnClickListener(v -> launchTransparentActivity());
+ }
+
+ private void launchTransparentActivity() {
+ startActivity(new Intent(this, TransparentActivity.class));
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java
index 49ac64c58cb8..1bac8bdb003a 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java
@@ -12,15 +12,18 @@
* 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.multishade.shared.model
+package com.android.server.wm.flicker.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
-import androidx.annotation.FloatRange
+public class TransparentActivity extends Activity {
-/** Models the current state of a shade. */
-data class ShadeModel(
- val id: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
-)
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.activity_transparent);
+ }
+}
diff --git a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
index 24a567130ff0..d3eeac147c2a 100644
--- a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
+++ b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt
@@ -129,7 +129,7 @@ class MotionPredictorTest {
// Prediction will happen for t=12 (since it is the next input interval after the requested
// time, 8, plus the model offset, 1).
assertEquals(12, predicted!!.eventTime)
- assertEquals(30f, predicted.x, /*delta=*/5f)
+ assertEquals(30f, predicted.x, /*delta=*/10f)
assertEquals(60f, predicted.y, /*delta=*/15f)
}
}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java
index 0c267b27490b..320daeeb2e54 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java
@@ -17,8 +17,9 @@
package com.android.inputmethod.stresstest;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
+import static com.android.compatibility.common.util.SystemUtil.eventually;
import static com.android.inputmethod.stresstest.ImeStressTestUtil.REQUEST_FOCUS_ON_CREATE;
import static com.android.inputmethod.stresstest.ImeStressTestUtil.TestActivity.createIntent;
import static com.android.inputmethod.stresstest.ImeStressTestUtil.callOnMainSync;
@@ -26,11 +27,16 @@ import static com.android.inputmethod.stresstest.ImeStressTestUtil.verifyWindowA
import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsHidden;
import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsShown;
+import static com.google.common.truth.Truth.assertWithMessage;
+
import android.content.Intent;
import android.platform.test.annotations.RootPermissionTest;
import android.platform.test.rule.UnlockScreenRule;
+import android.support.test.uiautomator.UiDevice;
import android.widget.EditText;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,6 +45,7 @@ import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/**
* Test IME visibility by using system default IME to ensure the behavior is consistent
@@ -59,8 +66,12 @@ public final class DefaultImeVisibilityTest {
public ScreenCaptureRule mScreenCaptureRule =
new ScreenCaptureRule("/sdcard/InputMethodStressTest");
+ private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(3);
+
private static final int NUM_TEST_ITERATIONS = 10;
+ private final boolean mIsPortrait;
+
@Parameterized.Parameters(name = "isPortrait={0}")
public static List<Boolean> isPortraitCases() {
// Test in both portrait and landscape mode.
@@ -68,6 +79,7 @@ public final class DefaultImeVisibilityTest {
}
public DefaultImeVisibilityTest(boolean isPortrait) {
+ mIsPortrait = isPortrait;
mImeStressTestRule.setIsPortrait(isPortrait);
}
@@ -75,14 +87,26 @@ public final class DefaultImeVisibilityTest {
public void showHideDefaultIme() {
Intent intent =
createIntent(
- 0x0, /* No window focus flags */
- SOFT_INPUT_STATE_UNSPECIFIED | SOFT_INPUT_ADJUST_RESIZE,
+ 0x0 /* No window focus flags */,
+ SOFT_INPUT_STATE_HIDDEN | SOFT_INPUT_ADJUST_RESIZE,
Collections.singletonList(REQUEST_FOCUS_ON_CREATE));
ImeStressTestUtil.TestActivity activity = ImeStressTestUtil.TestActivity.start(intent);
EditText editText = activity.getEditText();
+
+ UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ eventually(
+ () ->
+ assertWithMessage("Display rotation should be updated.")
+ .that(uiDevice.getDisplayRotation())
+ .isEqualTo(mIsPortrait ? 0 : 1),
+ TIMEOUT);
+
for (int i = 0; i < NUM_TEST_ITERATIONS; i++) {
- callOnMainSync(activity::showImeWithInputMethodManager);
+ // TODO(b/291752364): Remove the explicit focus request once the issue with view focus
+ // change between fullscreen IME and actual editText is fixed.
+ callOnMainSync(editText::requestFocus);
verifyWindowAndViewFocus(editText, true, true);
+ callOnMainSync(activity::showImeWithInputMethodManager);
waitOnMainUntilImeIsShown(editText);
callOnMainSync(activity::hideImeWithInputMethodManager);
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java
index 12104b298dac..c7463218b646 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java
@@ -20,9 +20,9 @@ import android.app.Instrumentation;
import android.os.RemoteException;
import android.support.test.uiautomator.UiDevice;
+import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
-import org.checkerframework.checker.nullness.qual.NonNull;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
@@ -35,8 +35,6 @@ import java.io.IOException;
public class ImeStressTestRule extends TestWatcher {
private static final String LOCK_SCREEN_OFF_COMMAND = "locksettings set-disabled true";
private static final String LOCK_SCREEN_ON_COMMAND = "locksettings set-disabled false";
- private static final String SET_PORTRAIT_MODE_COMMAND = "settings put system user_rotation 0";
- private static final String SET_LANDSCAPE_MODE_COMMAND = "settings put system user_rotation 1";
private static final String SIMPLE_IME_ID =
"com.android.apps.inputmethod.simpleime/.SimpleInputMethodService";
private static final String ENABLE_IME_COMMAND = "ime enable " + SIMPLE_IME_ID;
@@ -44,8 +42,10 @@ public class ImeStressTestRule extends TestWatcher {
private static final String DISABLE_IME_COMMAND = "ime disable " + SIMPLE_IME_ID;
private static final String RESET_IME_COMMAND = "ime reset";
- @NonNull private final Instrumentation mInstrumentation;
- @NonNull private final UiDevice mUiDevice;
+ @NonNull
+ private final Instrumentation mInstrumentation;
+ @NonNull
+ private final UiDevice mUiDevice;
// Whether the screen orientation is set to portrait.
private boolean mIsPortrait;
// Whether to use a simple test Ime or system default Ime for test.
@@ -105,12 +105,13 @@ public class ImeStressTestRule extends TestWatcher {
private void setOrientation() {
try {
mUiDevice.freezeRotation();
- executeShellCommand(
- mIsPortrait ? SET_PORTRAIT_MODE_COMMAND : SET_LANDSCAPE_MODE_COMMAND);
- } catch (IOException e) {
- throw new RuntimeException("Could not set screen orientation.", e);
+ if (mIsPortrait) {
+ mUiDevice.setOrientationNatural();
+ } else {
+ mUiDevice.setOrientationLeft();
+ }
} catch (RemoteException e) {
- throw new RuntimeException("Could not freeze rotation.", e);
+ throw new RuntimeException("Could not freeze rotation or set screen orientation.", e);
}
}
@@ -147,7 +148,8 @@ public class ImeStressTestRule extends TestWatcher {
}
}
- private @NonNull String executeShellCommand(@NonNull String cmd) throws IOException {
+ @NonNull
+ private String executeShellCommand(@NonNull String cmd) throws IOException {
return mUiDevice.executeShellCommand(cmd);
}
}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
index f3c81942ae42..c9b5c96c9920 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
@@ -392,7 +392,7 @@ public final class ImeStressTestUtil {
public boolean showImeWithInputMethodManager() {
boolean showResult =
getInputMethodManager()
- .showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
+ .showSoftInput(mEditText, 0 /* flags */);
if (showResult) {
Log.i(TAG, "IMM#showSoftInput successfully");
} else {
@@ -404,7 +404,8 @@ public final class ImeStressTestUtil {
/** Hide IME with InputMethodManager. */
public boolean hideImeWithInputMethodManager() {
boolean hideResult =
- getInputMethodManager().hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
+ getInputMethodManager()
+ .hideSoftInputFromWindow(mEditText.getWindowToken(), 0 /* flags */);
if (hideResult) {
Log.i(TAG, "IMM#hideSoftInput successfully");
} else {
diff --git a/tests/Internal/src/android/service/wallpaper/OWNERS b/tests/Internal/src/android/service/wallpaper/OWNERS
new file mode 100644
index 000000000000..5a26d0e1f62b
--- /dev/null
+++ b/tests/Internal/src/android/service/wallpaper/OWNERS
@@ -0,0 +1,4 @@
+dupin@google.com
+santie@google.com
+pomini@google.com
+poultney@google.com \ No newline at end of file
diff --git a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java
index 153ca79e346b..0c5e8d481131 100644
--- a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java
+++ b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java
@@ -85,4 +85,17 @@ public class WallpaperServiceTest {
assertEquals("onAmbientModeChanged should have been called", 2, zoomChangedCount[0]);
}
+ @Test
+ public void testNotifyColorsOfDestroyedEngine_doesntCrash() {
+ WallpaperService service = new WallpaperService() {
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+ };
+ WallpaperService.Engine engine = service.onCreateEngine();
+ engine.detach();
+
+ engine.notifyColorsChanged();
+ }
}
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index edd6dd3468ef..82e40b1eee6b 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -32,6 +32,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* This is a wrapper around {@link TestLooperManager} to make it easier to manage
@@ -55,7 +56,6 @@ public class TestableLooper {
private MessageHandler mMessageHandler;
private Handler mHandler;
- private Runnable mEmptyMessage;
private TestLooperManager mQueueWrapper;
static {
@@ -121,8 +121,12 @@ public class TestableLooper {
* @param num Number of messages to parse
*/
public int processMessages(int num) {
+ return processMessagesInternal(num, null);
+ }
+
+ private int processMessagesInternal(int num, Runnable barrierRunnable) {
for (int i = 0; i < num; i++) {
- if (!parseMessageInt()) {
+ if (!processSingleMessage(barrierRunnable)) {
return i + 1;
}
}
@@ -130,6 +134,27 @@ public class TestableLooper {
}
/**
+ * Process up to a certain number of messages, not blocking if the queue has less messages than
+ * that
+ * @param num the maximum number of messages to process
+ * @return the number of messages processed. This will be at most {@code num}.
+ */
+
+ public int processMessagesNonBlocking(int num) {
+ final AtomicBoolean reachedBarrier = new AtomicBoolean(false);
+ Runnable barrierRunnable = () -> {
+ reachedBarrier.set(true);
+ };
+ mHandler.post(barrierRunnable);
+ waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
+ try {
+ return processMessagesInternal(num, barrierRunnable) + (reachedBarrier.get() ? -1 : 0);
+ } finally {
+ mHandler.removeCallbacks(barrierRunnable);
+ }
+ }
+
+ /**
* Process messages in the queue until no more are found.
*/
public void processAllMessages() {
@@ -165,19 +190,20 @@ public class TestableLooper {
private int processQueuedMessages() {
int count = 0;
- mEmptyMessage = () -> { };
- mHandler.post(mEmptyMessage);
- waitForMessage(mQueueWrapper, mHandler, mEmptyMessage);
- while (parseMessageInt()) count++;
+ Runnable barrierRunnable = () -> { };
+ mHandler.post(barrierRunnable);
+ waitForMessage(mQueueWrapper, mHandler, barrierRunnable);
+ while (processSingleMessage(barrierRunnable)) count++;
return count;
}
- private boolean parseMessageInt() {
+ private boolean processSingleMessage(Runnable barrierRunnable) {
try {
Message result = mQueueWrapper.next();
if (result != null) {
// This is a break message.
- if (result.getCallback() == mEmptyMessage) {
+ if (result.getCallback() == barrierRunnable) {
+ mQueueWrapper.execute(result);
mQueueWrapper.recycle(result);
return false;
}
diff --git a/tests/testables/tests/src/android/testing/TestableLooperTest.java b/tests/testables/tests/src/android/testing/TestableLooperTest.java
index 0f491b86626c..a02eb6b176dc 100644
--- a/tests/testables/tests/src/android/testing/TestableLooperTest.java
+++ b/tests/testables/tests/src/android/testing/TestableLooperTest.java
@@ -27,12 +27,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -40,6 +34,11 @@ import android.test.suitebuilder.annotation.SmallTest;
import android.testing.TestableLooper.MessageHandler;
import android.testing.TestableLooper.RunWithLooper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@@ -240,4 +239,33 @@ public class TestableLooperTest {
inOrder.verify(handler).dispatchMessage(messageC);
}
+ @Test
+ public void testProcessMessagesNonBlocking_onlyArgNumber() {
+ Handler h = new Handler(mTestableLooper.getLooper());
+ Runnable r = mock(Runnable.class);
+
+ h.post(r);
+ h.post(r);
+ h.post(r);
+
+ int processed = mTestableLooper.processMessagesNonBlocking(2);
+
+ verify(r, times(2)).run();
+ assertEquals(2, processed);
+ }
+
+ @Test
+ public void testProcessMessagesNonBlocking_lessMessagesThanArg() {
+ Handler h = new Handler(mTestableLooper.getLooper());
+ Runnable r = mock(Runnable.class);
+
+ h.post(r);
+ h.post(r);
+ h.post(r);
+
+ int processed = mTestableLooper.processMessagesNonBlocking(5);
+
+ verify(r, times(3)).run();
+ assertEquals(3, processed);
+ }
}